
Lisp is often advertised as a language with advantages over the rest because it has some unique, well-integrated and useful features.
The following is an attempt to highlight a set of features of standard Common Lisp, briefly and with examples.
This article is likely to be most useful to those who have some programming experience, are interested in Lisp and want to better understand what makes it so attractive.
')
The text is largely based on a list of CL features and a CL review by Robert Strandh.
Rich and accurate arithmetic
Lisp provides a rich hierarchy of numeric types that are well integrated with the rest of the language.
Long numbers (bignums) are created automatically as needed, which reduces the risk of overflows and ensures accuracy. For example, we can quickly calculate the value 10 ↑↑ 4:
> (expt (expt (expt (expt 10 10) 10) 10) 10) 100000000000000000000000000000000000[...]
Rational numbers are represented as fractions, so no rounding error occurs when using them. Accurate rational arithmetic is integrated into the language:
> (+ 5/9 3/4) 47/36
Complex numbers are also a built-in data type in a lisp. They can be presented in the form of a short syntax: #c (10 5) means 10 + 5i. Arithmetic operations can also work with complex values:
> (* 2 (+ #c(10 5) 4)) #C(28 10)
Generalized links
Forms or
positions can be used as if they were separate variable variables. Using SETF and other similar constructions, you can change the values that are conceptually related to a given position.
For example, you can use SETF as follows:
> (defvar *colours* (list 'red 'green 'blue)) *COLOURS* > (setf (first *colours*) 'yellow) YELLOW > *colours* (YELLOW BLUE GREEN)
And PUSH - so:
> (push 'red (rest *colours*)) (RED BLUE GREEN) > *colours* (YELLOW RED BLUE GREEN)
Generalized links work not only when applied to lists, but also to many other types of structures and objects. For example, in object-oriented programs, one of the ways to change some field of an object is using SETF.
Plural values
Values can be combined without explicitly creating a structure, such as a list. For example, (values 'foo' bar) returns two values - 'foo and' bar. Using this mechanism, functions can return several values at once, which can simplify the program.
For example, FLOOR is a standard function that returns two values:
> (floor pi) 3 0.14159265358979312d0
By convention, functions that return multiple values are used by default as if only one value was returned — the first.
> (+ (floor pi) 2) 5
In this case, you can explicitly get and use the other values. In the following example, we separate the integer and fractional parts of PI when rounding:
> (multiple-value-bind (integral fractional) (floor pi) (+ integral fractional)) 3.141592653589793d0
Macros
A macro in a lisp is a kind of function that takes lisp forms or objects as arguments and, as a rule, generates code that will then be compiled and executed. This happens before the program is executed, during a phase called
macro expansion (macroexpansion). Macros can perform some calculations during the sweep, using the full capabilities of the language.
One of the uses of macros is to convert any source code into a representation that is correct in terms of already existing definitions. In other words, macros allow you to add a new syntax to the language (this approach is known as
syntactic abstraction ).
This allows you to easily embed domain-specific languages (DSL) in Lisp, since a special syntax can be added to the language before executing the program.
The main benefit of using macros is that they extend the capabilities of the language, allowing the programmer to express his ideas more easily and with less code. You can add new tools to the language as if they were embedded. In addition, if macros are used for preliminary data calculation or initialization, they can help optimize performance.
LOOP macro
The LOOP macro is a powerful tool for representing loops. In fact, it is a whole small embedded language for describing iterative processes. LOOP provides all the necessary types of expressions for writing loops, from simple iterations to iterators to complex finite automata.
> (defvar *list* (loop :for x := (random 1000) :repeat 5 :collect x)) *LIST* > *list* (324 794 102 579 55)
> (loop :for elt :in *list* :when (oddp elt) :maximizing elt) 579
> (loop :for elt :in *list* :collect (log elt) :into logs :finally (return (loop :for l :in logs :if (> l 5.0) :collect l :into ms :else :collect l :into ns :finally (return (values ms ns))))) (5.7807436 6.6770835 6.3613024) (4.624973 4.0073333)
FORMAT function
The FORMAT function supports a built-in language for describing how data should be formatted. In addition to simple textual substitution, FORMAT statements can express in a compact form various rules for generating text, such as conditions, cycles, and handling of boundary cases.
We can format the name list with this function:
(defun format-names (list) (format nil "~{~:(~a~)~#[.~; and ~:;, ~]~}" list))
> (format-names '(doc grumpy happy sleepy bashful sneezy dopey)) "Doc, Grumpy, Happy, Sleepy, Bashful, Sneezy and Dopey." > (format-names '(fry laurie)) "Fry and Laurie." > (format-names '(bluebeard)) "Bluebeard."
FORMAT transfers its result to the specified stream, be it standard output to the screen, a string, or any other stream.
Higher order functions
Functions in Lisp are real essential first class. Functional objects can be dynamically created, transmitted as parameters, or returned as a result. Thus,
higher-order functions are supported, that is, such arguments and whose returned values can themselves be functions.
Here you see the call to the SORT function, whose arguments are a list and another function (in this case # '<):
> (sort (list 4 2 3 1) #'<) (1 2 3 4)
Anonymous functions , also called lambda expressions, can be used instead of the name of the function being passed. They are especially useful when you want to create a function for one-time use, without clogging the program with an unnecessary name. In general, they can be used to create lexical closures.
In this example, we create an anonymous function to use it as the first argument of MAPCAR:
> (mapcar (lambda (x) (+ x 10)) '(1 2 3 4 5)) (11 12 13 14 15)
When creating a function, they capture the context, which allows us to use full-fledged lexical closures:
(let ((counter 10)) (defun add-counter (x) (prog1 (+ counter x) (incf counter))))
> (mapcar #'add-counter '(1 1 1 1)) (11 12 13 14) > (add-counter 50) 64
Processing lists
Since lists are the fundamental built-in data type in Lisp, there is an extensive set of functions for manipulating lists. Thanks to these functions and macros, lists can be used to quickly prototype other data structures.
For example, we can work with a regular list like this:
> (defvar *nums* (list 0 1 2 3 4 5 6 7 8 9 10 11 12)) *NUMS* > (list (fourth *nums*) (nth 8 *nums*)) (3 8) > (list (last *nums*) (butlast *nums*)) ((12) (0 1 2 3 4 5 6 7 8 9 10 11)) > (remove-if-not #'evenp *nums*) (0 2 4 6 8 10 12)
And so - with an associative list
> (defvar *capital-cities* '((NZ . Wellington) (AU . Canberra) (CA . Ottawa))) *CAPITAL-CITIES* > (cdr (assoc 'CA *capital-cities*)) OTTAWA > (mapcar #'car *capital-cities*) (NZ AU CA)
Lambda lists
The lambda list sets the parameters for functions, macros, binding forms and some other constructions. Lambda lists define mandatory, optional, named, tail (rest) and optional parameters, as well as defaults and the like. This allows you to define very flexible and expressive interfaces.
Optional parameters do not require the caller to specify any value. For them, a default value can be defined, otherwise, the called code can check whether a value has been provided and act on the situation.
The following function accepts an optional delimiter parameter, the default for which is a whitespace character:
(defun explode (string &optional (delimiter #\Space)) (let ((pos (position delimiter string))) (if (null pos) (list string) (cons (subseq string 0 pos) (explode (subseq string (1+ pos)) delimiter)))))
When calling the EXPLODE function, we can either provide an optional parameter or omit it.
> (explode "foo, bar, baz" #\,) ("foo " " bar " " baz")
> (explode "foo, bar, baz") ("foo," "bar," "baz")
Named parameters are similar to optional parameters, but they can be passed in any order, since they are defined by names. The use of names improves the readability of the code and serves as a kind of documentation when you make a call with several parameters.
For example, compare these two function calls:
// In C: xf86InitValuatorAxisStruct(device, 0, 0, -1, 1, 0, 1)
Symbols as first-class entities
Symbols are unique objects completely defined by their names. Let's say 'foo is a symbol whose name is “FOO”. Symbols can be used as identifiers or as certain abstract names. Comparison of characters occurs in a fixed time.
Symbols, like functions, are first-class entities. They can be dynamically created, quoted (quote, unevaluate), stored, passed as arguments, compared, converted into strings, exported and imported, they can be referenced.
Here '* foo * is the variable identifier:
> (defvar *foo* 5) *FOO* > (symbol-value '*foo*) 5
Packages as first class entities
Packages that play the role of namespaces (namespaces) are also first class objects. Since they can be created, stored, returned as a result at runtime, it is possible to dynamically switch the context or convert namespaces dynamically.
In the following example, we use INTERN to include a symbol in some package:
> (intern "ARBITRARY" (make-package :foo :use '(:cl))) FOO::ARBITRARY NIL
There is a special variable * package * in the list that indicates the current package. Let's say if the current packet is FOO, then you can run:
> (in-package :foo) #<PACKAGE "FOO"> > (package-name *package*) "FOO"
Special variables
Lisp supports dynamic context variables in addition to lexical context. Dynamic variables in some cases can be useful, so their support allows for maximum flexibility.
For example, we can redirect the output of some code to a nonstandard stream, such as a file, by creating a dynamic link for the special variable * standard-output *:
(with-open-file (file-stream #p"somefile" :direction :output) (let ((*standard-output* file-stream)) (print "This prints to the file, not stdout.")) (print "And this prints to stdout, not the file."))
In addition to * standard-output *, Lisp includes several special variables that store program state, including resources and parameters, such as * standard-input *, * package *, * readtable *, * print-readably *, * print-circle * etc.
Transfer of control
In Lisp, there are two ways to transfer control to a point higher in the call hierarchy. In this case, the lexical or dynamic domain can be taken into account for local and non-local transitions, respectively.
Named blocks allow a nested form to return control from any named parent form using BLOCK and RETURN-FROM.
For example, here the nested loop returns a list from the early block to bypass the outer loop:
> (block early (loop :repeat 5 :do (loop :for x :from 1 :to 10 :collect x :into xs :finally (return-from early xs)))) (1 2 3 4 5 6 7 8 9 10)
Catch / throw is something like a non-local goto. THROW jumps to the last CATCH encountered and passes the value that was specified as a parameter.
In the THROW-RANGE function, based on the previous example, we can apply THROW and CATCH using the dynamic state of the program.
(defun throw-range (ab) (loop :for x :from a :to b :collect x :into xs :finally (throw :early xs)))
> (catch :early (loop :repeat 5 :do (throw-range 1 10))) (1 2 3 4 5 6 7 8 9 10)
When it is enough to use the lexical scope and catch / throw, when it is necessary to take into account the dynamic state.
Conditions restart
A system of conditions in a Lisp is a mechanism for transmitting signals between parts of a program.
One possible use is to throw exceptions and handle them, much like it does in Java or Python. But, unlike other languages, during the signal transmission in the Lisp, the stack
does not unfold , therefore all data is saved and the signal handler can restart the program from any point in the stack.
This approach to the handling of exceptional situations allows to improve the separation of tasks and thus achieve greater structuring of the code. But such a mechanism has a wider scope, as a system for transmitting arbitrary messages (and not just errors) between parts of a program.
An example of using the condition system can be found in the
Common Lisp article
: A Tutorial on Conditions and Restarts .
Generic functions
The Common Lisp object system (Common Lisp Object System, CLOS) does not bind methods to classes, but allows the use of generic functions.
Generic functions define signatures that can be satisfied by several different methods. When calling, the method that best matches the arguments is selected.
Here we define a generic function that handles keyboard events:
(defgeneric key-input (key-name))
Then we define several methods that satisfy different KEY-NAME values.
(defmethod key-input (key-name)
Let's look at the method call in action:
> (key-input :space) "Space key pressed" > (key-input :return) "No keybinding for RETURN" > (defmethod key-input ((key-name (eql :return))) (format nil "Return key pressed")) > (key-input :return) "Return key pressed"
We did without constructions a la switch and explicit work with the table of methods. Thus, we can add processing of new special cases independently, dynamically, as needed, and generally at any point of the program. This, in particular, ensures the development of programs on the bottom-up Lisp.
Generic functions define some common characteristics of a group of methods. For example, methods of combining methods, specialization options, and other properties can be set by a generalized function.
Lisp provides many useful standard generic functions; an example is PRINT-OBJECT, which can be specialized for any class to set its textual representation.
Combinations of methods
Combinations of methods allow for calling a method to perform a whole
chain of methods, either in a certain order or so that some functions process the results of others.
There are built-in ways of combining methods that line up the methods in a given order. Methods equipped with keywords: before,: after or: around are placed in the appropriate place in the call chain.
For example, in the previous example, each of the KEY-INPUT methods repeats the output of the phrase "key pressed". We can improve the code with a combination like: around
(defmethod key-input :around (key-name) (format nil "~:(~a~) key pressed" (call-next-method key-name)))
After that, we will redefine the KEY-INPUT methods, specifying only one line in each of them:
(defmethod key-input ((key-name (eql :escape))) "escape")
When KEY-INPUT is called, the following happens:
- the method with a tag is called: around
- it calls the following method, that is, one of the specialized versions of KEY-INPUT,
- which returns a string, and this string formats the c: around method.
It should be noted that the default option can be processed in different ways. We can simply use the THROW / CATCH pair (a more advanced implementation could use conditions):
(defmethod key-input (key-name) (throw :default (format nil "No keybinding for ~a" key-name))) (defmethod key-input :around (key-name) (catch :default (format nil "~:(~a~) key pressed" (call-next-method key-name))))
As a result, the built-in method of combining methods allows us to generalize the processing of events from the keyboard into a modular, extensible, easily changeable mechanism. This technique can be complemented with user-defined combination methods; let's say you can add a combination method that will perform the summation or sorting of the results of the methods.
Multiple inheritance
Any class can have many ancestors, which allows you to create richer models and achieve more efficient code reuse. The behavior of the child classes is determined in accordance with the order, which is based on the definitions of ancestor classes.
With the help of combinations of methods, metaobject protocol and other features of CLOS, you can bypass the traditional problems of multiple inheritance (such as fork-join).
Meta-Object Protocol
Meta-object protocol (Meta-object protocol, MOP) is a software interface to CLOS, which itself is implemented using CLOS. MOP allows programmers to explore, use and modify the internal structure of CLOS through CLOS itself.
Classes as first class entities
Classes themselves are also objects. Using MOP, you can change the definition and behavior of classes.
Let the class FOO be a descendant of the class BAR, then we can add, say, the class BAZ to the list of ancestors FOO using the ENSURE-CLASS function:
(defclass bar () ()) (defclass foo (bar) ()) (defclass baz () ())
> (class-direct-superclasses (find-class 'foo)) (#<STANDARD-CLASS BAR>) > (ensure-class 'foo :direct-superclasses '(bar baz)) #<STANDARD-CLASS FOO> > (class-direct-superclasses (find-class 'foo)) (#<STANDARD-CLASS BAR> #<STANDARD-CLASS BAZ>)
We used the CLASS-DIRECT-SUPERCLASSES function to get information about the ancestors of the class; in this case, it takes as its argument a class in the form of an object obtained from FIND-CLASS.
The above example illustrates the mechanism by which classes can be modified during program execution, which allows, among other things, to dynamically add impurities to the classes (mixins).
Dynamic overrides
Lisp is a very interactive and dynamic environment. Functions, macros, classes, packages, parameters, and objects can be redefined at almost any time, and the result will be adequate and predictable.
So, if you redefine a class during program execution, the changes will immediately be applied to all objects and subclasses of this class. We can define a BALL class with the radius property and its subclass TENNIS-BALL:
> (defclass ball () ((%radius :initform 10 :accessor radius))) #<STANDARD-CLASS BALL> > (defclass tennis-ball (ball) ()) #<STANDARD-CLASS TENNIS-BALL>
Here is an object of the TENNIS-BALL class, it has a slot for the radius property:
> (defvar *my-ball* (make-instance 'tennis-ball)) *MY-BALL* > (radius *my-ball*) 10
And now we can redefine the BALL class by adding another volume slot to it:
> (defclass ball () ((%radius :initform 10 :accessor radius) (%volume :initform (* 4/3 pi 1e3) :accessor volume))) #<STANDARD-CLASS BALL>
And * MY-BALL * was automatically updated, having received a new slot, which was defined in the ancestor class.
> (volume *my-ball*) 4188.790204786391d0
Access to the compiler at runtime
Thanks to the COMPILE and COMPILE-FILE functions, the lisp compiler can be directly used from the executable program. Thus, functions that are created or modified while the program is running can also be compiled.
It turns out that programs can be compiled step by step, which makes development interactive, dynamic and fast. Running programs can be changed, debugged and grow gradually.
Compilation macros
Compilation macros define alternative strategies for compiling a function or macro. Unlike conventional macros, a compilation macro does not extend the syntax of the language and can only be applied at compile time. , .
— — . , , , .
, EXPLODE, :
(defun explode (string &optional (delimiter #\Space)) (declare (type character delimiter) (type string string)) ...)
. , S-. .
, READ, READ-CHAR, READ-LINE, READ-FROM-STRING .. , , , , .
READ-FROM-STRING, (400 500 600), , "(400 500 600)".
> (read-from-string "(400 500 600)") (400 500 600) 13 > (type-of (read-from-string "t")) BOOLEAN
(reader macros) . , . — ( , ).
:
- #'foo — ,
- #\\ — (characters),
- #c(4 3) — ,
- #p"/path/" — .
, ; , . , , (read-eval-print loop, REPL).
:
> (read-from-string "#xBB") 187
, - .
PRINT-OBJECT — , , . , , , FORMAT, PRINT REPL.
JOURNEY:
(defclass journey () ((%from :initarg :from :accessor from) (%to :initarg :to :accessor to) (%period :initarg :period :accessor period) (%mode :initarg :mode :accessor mode)))
JOURNEY, :
> (defvar *journey* (make-instance 'journey :from "Christchurch" :to "Dunedin" :period 20 :mode "bicycle")) *JOURNEY* > (format nil "~a" *journey*) "#<JOURNEY {10044DCCA1}>"
PRINT-OBJECT JOURNEY, - :
(defmethod print-object ((j journey) (s stream)) (format s "~A to ~A (~A hours) by ~A." (from j) (to j) (period j) (mode j)))
:
> (format nil "~a" *journey*) "Christchurch to Dunedin (20 hours) by bicycle."