Some time ago (about three years) I decided to read a textbook on Lisp. Without any specific goal, just for the sake of common development and the possibility of shocking interlocutors with exotic (once it seems, it even happened).
But on closer examination, Lisp turned out to be really powerful, flexible and oddly useful in “everyday life”. All the small tasks of automation quickly migrated to scripts on Lisp, as well as the possibilities for automation more complex.
Here it is worth noting that by “the possibility of automation” I mean a situation where the total time for writing and debugging a program is less than the time spent manually solving the same task.
')
Paul Graham wrote more than one article and even a book about the benefits of Lisp. At the time of this writing, Lisp is ranked 33rd in the TOIBE ranking (three times as dead as Delphi is dead). The question arises: why is language so little common if it is so convenient? About two years of use gave a few hints about the reasons.
disadvantages
1. Shared data structuresA concept that allows optimizing functional programs, but fraught with subtle errors in imperative ones. The possibility of accidental damage to an extraneous data structure when modifying a variable that has no visible connection with the structure requires the programmer to continuously monitor what is happening behind the scenes and knowledge of the internal implementation of each function used (both system and user). The most surprising thing is the possibility of damaging the function body, by modifying its return value.
2. Lack of encapsulationAlthough the concept of a package exists, it has nothing to do with a
package in Ada or a
unit in Delphi. Any code can add anything to any package (except system). Any code can extract anything from any package using the
:: operator.
3. Unsystematic abbreviationsWhat is the difference between MAPCAN and MAPCON? Why in SETQ, the last letter Q? Given the age of the language, you can understand the reasons for this state of affairs, but I want the language a little cleaner.
4. MultithreadingThis disadvantage is indirectly related to Lisp and mainly concerns the implementation I use - SteelBank Common Lisp. Common Lisp does not provide multithreading. An attempt to use the implementation provided by SBCL did not lead to success.
It is a pity to refuse such a convenient tool, but dissatisfaction gradually accumulates.
Finding a solution
First you can go to Wikipedia on the Lisp page. See the section "Dialects". Read a brief introduction to each. And realize that the taste and color of all the markers are different.
If you want to do something, you need to do it yourself.
- Jean Baptiste Emmanuel Sorg
Let's try to create your correct Lisp, adding to it a bit of Ada, a lot of Delphi and quite a drop of Oberon. Let's call the resulting mixture Lysya.
Basic concepts
1. No pointersAs part of the fight against PROBLEM-1, all operations must be carried out by copying the values. By the type of data structure in the code or when printing, all its properties, external and internal connections must be fully visible.
2. Add modulesTo combat problem-2, we import
package ,
with, and
use operators from Ada. In the process, discard the redundantly complex Lisp symbol import / shading scheme.
(package - ( ) () ())
(with -)
(use -)
3. Less abbreviationsThe most frequent and commonly used characters will still be abbreviated, but mostly the most obvious:
const ,
var . The formatted output function - FMT requires reduction, since it is often found inside expressions.
Elt - taking an element - leaked from Common Lisp and caught on, although there is no need to cut it.
4. Register-independent identifiersI believe that the correct language (and file system) {$ HOLYWAR +} must be case-insensitive {$ HOLYWAR-}, so as not to break my head once more.
5. Ease of use with Russian keyboard layoutLisi syntax avoids the use of characters that are not available in one of the layouts. No square and curly braces. No #, ~, &, <,>, |. When reading numeric literals, both comma and period are considered valid decimal delimiters.
6. Extended alphabetOne of the nice features of SBCL was UTF-8 in code. The ability to declare constants BEAR, VODKA and BALALAYK greatly simplifies writing application code. The ability to insert Ω, Ψ and Σ makes formulas in the code more visible. Although it is theoretically possible to use any Unicode characters, it is difficult to guarantee the correctness of working with them (rather laziness than difficult). We restrict ourselves to Cyrillic, Latin and Greek.
7. Numeric LiteralsThis is the most useful language extension for me.
10_000
The last option seems to me not the most aesthetic, but it is the most popular.
8. CyclesLoops in Lisp are non-standard and pretty messy. Simplify to the minimum standard set.
(for i 5
Variable loop outside is not visible.
(while )
9. GOTONot very necessary operator, but without it, it is difficult to demonstrate neglect of the rules of structured programming.
(block : (goto :))
10. Unification of scopesLisp has two different types of scopes: TOPLEVEL and local. Accordingly, there are two different ways to declare variables.
(defvar A 1) (let ((a 1)) …)
In Fox, there is only one way used both at the top level of the script and in local areas, including packages.
(var A 1)
If necessary, limit the scope of the operator is used
(block (var A 1) (set A 2) (fmt nil A))
The body of the loop is contained in the implicit BLOCK statement (as is the body of the function / procedure). All variables declared in the loop are destroyed at the end of the iteration.
11. Single-slot characterIn Lisp, functions are special objects and are stored in a special symbol slot. One character can simultaneously store a variable, a function, and a property list. In Fox, each character is associated with only one value.
12. Convenient ELTTypical access to a complex structure element in Lisp looks like this.
(elt (slot-value (elt 1) '-2) 3)
The Fox implements a unified ELT operator that provides access to the elements of any composite types (lists, rows, records, byte arrays, hash tables).
(elt 1 \-2 3)
Identical functionality can also be obtained on a Lisp macro.
(defmacro field (object &rest f) " . (field *object* 0 :keyword symbol \"string\") . plist. ( ) . ." (if f (symbol-macrolet ((f0 (elt f 0))(rest (subseq f 1))) (cond ((numberp f0) `(field (elt ,object ,f0) ,@rest)) ((keywordp f0) `(field (getf ,object ,f0) ,@rest)) ((stringp f0) `(field (cdr (assoc ,f0 ,object :test 'equal)) ,@rest)) ((and (listp f0) (= 2 (length f0))) `(field (,(car f0) ,(cadr f0) ,object) ,@rest)) ((symbolp f0) `(field (,f0 ,object) ,@rest)) (t `(error " ")))) object))
13. Restriction of subprogram parameters transfer modesIn Lisp, there are at least five modes of parameter transfer: mandatory,
& optional ,
& rest ,
& key ,
& whole and their arbitrary combination is allowed. In fact, most combinations have strange effects.
In Fox, you can only use a combination of the required parameters and one of the following modes to choose from
: key ,: optional,: flag ,: rest .
14. MultithreadingIn order to simplify the writing of multi-threaded programs to the utmost, the concept of the separation of memory was adopted. When spawning a thread, all variables available to the new thread are copied. All references to these variables are replaced by references to copies. Information transfer between streams is possible only by means of protected objects or through the result returned by the stream upon completion.
Protected objects always contain critical sections to ensure atomic operations. Entry into critical sections is automatic - there are no separate operators for this in the language. Protected objects include: message queue, console, and file descriptors.
Creating streams is possible with a multi-threaded mapping function.
(map-th (function (x) …) --)
Map-th automatically starts the number of threads equal to the number of processors in the system (or two times more if you have Intel inside). For a recursive call, subsequent map-th calls work as one stream.
Additionally, there is a built-in thread function that performs the procedure / function in a separate thread.
15. Functional purity in the imperative codeThe Fox has functions for functional programming and procedures for procedural. Subroutines declared using the function keyword are subject to the absence of side effects and the independence of the result from external factors.
Unrealized
Some of the interesting features of the Lisp remained unrealized due to low priority.
1. Generalized methodsAbility to perform function overloading with defgeneric / defmethod.
2. Inheritance3. Built-in debuggerWhen an exception occurs, the Lisp interpreter switches to debug mode.
4. UFFIInterface for connecting modules written in other languages.
5. BIGNUMSupport for integers of arbitrary width
DiscardedSome features of Lisp were considered and deemed worthless / harmful.
1. Controlled combination of methodsWhen calling a method for a class, a combination of parent methods is performed and it is possible to change the combination rules. The final behavior of the method seems poorly predictable.
2. RestartsAn exception handler can make changes to the program state and send a restart command to the code that generated the exception. The effect of the application is similar to the use of the operator GOTO to move from function to function.
3. Rome accountLisp maintains a number system that was outdated shortly before its appearance.
Using
I'll give you some simple code examples.
(function crc8 (data :optional seed) (var result (if-nil seed 0)) (var s_data data) (for bit 8 (if (= (bit-and (bit-xor result s_data) $01) 0) (set result (shift result -1 8)) (else (set result (bit-xor result $18)) (set result (shift result -1 8)) (set result (bit-or result $80)))) (set s_data (shift s_data -1 8))) result)
Implementation
The interpreter is written in Delphi (FreePascal in compatibility mode). It is built in Lazarus 1.6.2 and higher, under Windows and Linux 32 and 64 bits. Of external dependencies requires libmysql.dll. It contains about 15_000..20_000 lines. There are about 200 built-in functions for various purposes (some are overloaded eight times).
Stored hereDynamic typing support is performed in a trivial way - all processed data types are represented by descendants of a single TValue class.
The most important type for Lisp - the list is, as is customary in Delphi, a class containing a dynamic array of objects of type TValue. For this type, the CopyOnWrite mechanism is implemented.
Memory management is automatic based on reference counting. For recursive structures, all references in the structure are calculated simultaneously. The freeing of memory starts immediately when the variables go out of scope. There are no deferred launch mechanisms for the garbage collector.
Exception handling works on the Delphi built-in mechanism. Thus, errors that occur in the interpreter code can be processed by the executable code on Fox.
Each Lisi operator or built-in function is implemented as a method or function in the interpreter code. The execution of the script is done by mutually recursive invoking implementations. The interpreter and script code has a common call stack.
Script variables are stored in dynamic memory independently. Each user-defined function has its own stack for storing references to variables, independent of the top-level stack or the stack of the parent functions.
Of particular difficulty is the implementation of the assignment operator (set) for elements of structures. Direct calculation of the pointer to the required element leads to the risk of hanging links, since the Lisi syntax does not prohibit the modification of the structure in the process of calculating the required element. As a compromise solution, a “chain pointer” is implemented - an object containing a reference to a variable and an array of numeric indices to indicate the path within the structure. Such a pointer is also subject to the problem of hanging links, but in the event of a failure, it generates a meaningful error message.
Development tools
1. Console2. Text editorEquipped with syntax highlighting and the ability to run an editable script on F9.

Conclusion
In the current state, the project solves those problems for which it was started, and does not require further active refinement. Many present deficiencies do not have a significant impact on the work.