LEARN X · ЗА 17 МИН
Common Lisp
Common Lisp за 17 минут: S-выражения, списки, функции, макросы, CLOS и REPL — весь язык на одной странице в комментариях кода.
Common Lisp — мультипарадигменный диалект Lisp из 1984 года (стандарт ANSI). Гомоиконность (код = данные), мощнейшая макросистема, объектная система CLOS и интерактивная разработка в REPL. Весь язык — ниже, прямо в комментариях кода.
Синтаксис: S-выражения
Всё в Lisp — это S-выражение: либо атом, либо список (оператор аргумент1 аргумент2 ...). Префиксная запись: оператор всегда первый.
; Это однострочный комментарий — точка с запятой до конца строки.
;; Принято: ;; для комментариев на уровне кода, ;;; для разделов файла.
#| Это блочный комментарий.
Может занимать
несколько строк. |#
; Вызов функции: круглые скобки, оператор впереди, потом аргументы.
(+ 1 2 3) ; => 6 (префиксная запись)
(* 2 (+ 3 4)) ; => 14 (вложенные выражения)
(- 10 1 2) ; => 7
; format — вывод. t значит «в стандартный поток», ~a — подставить значение,
; ~% — перевод строки.
(format t "Привет, ~a!~%" "мир") ; печатает: Привет, мир!
(format t "Сумма = ~a~%" (+ 2 3)) ; печатает: Сумма = 5
Значения и переменные
Глобальные — через defparameter/defvar, локальные — через let. Присваивание — setf.
; Глобальная переменная (динамическая). Звёздочки *...* — соглашение об именах.
(defparameter *name* "Аня") ; всегда переопределяет значение
(defvar *count* 0) ; задаёт значение ТОЛЬКО если ещё не связана
; setf — универсальное присваивание (set field).
(setf *name* "Боря")
(setf *count* (+ *count* 1))
; let — локальные переменные, видимы только внутри тела.
(let ((x 10)
(y 20))
(+ x y)) ; => 30
; let* — как let, но переменные видят предыдущие в том же блоке.
(let* ((a 2)
(b (* a 3))) ; b видит a
(+ a b)) ; => 8
; Имена могут содержать дефисы, ?, ! и т.п. — это нормально.
(defparameter max-retries 3)
(defparameter valid-input? t)
Типы данных
; Числа: целые произвольной точности, дроби, с плавающей точкой, комплексные.
42 ; целое
(* 1000000000000 1000000000000) ; => огромное число, без переполнения
1/3 ; точная дробь (рациональное)
(+ 1/3 1/6) ; => 1/2 (точная арифметика)
3.14 ; число с плавающей точкой
#c(1 2) ; комплексное число 1+2i
; Строки — в двойных кавычках.
"Привет"
(concatenate 'string "abc" "def") ; => "abcdef"
(length "Lisp") ; => 4
; Символы (symbols) — идентификаторы; по умолчанию приводятся к ВЕРХНЕМУ регистру.
'foo ; символ FOO (кавычка ' = «не вычислять»)
(symbol-name 'foo) ; => "FOO"
; Ключевые слова — символы, начинающиеся с двоеточия; вычисляются сами в себя.
:north ; => :NORTH (удобны как метки/опции)
; Логические значения: nil — это и ложь, и пустой список; всё прочее — истина.
nil ; ложь / пустой список / ()
t ; канонический «истина»
(if nil "да" "нет") ; => "нет"
(if 0 "да" "нет") ; => "да" (0 — это истина!)
Списки
Список — основа Lisp. Строится из пар cons. car — голова, cdr — хвост.
; cons создаёт пару (ячейку). list — список из нескольких элементов.
(cons 1 2) ; => (1 . 2) точечная пара
(cons 1 (cons 2 nil)) ; => (1 2) список заканчивается на nil
(list 1 2 3) ; => (1 2 3)
; Кавычка ' — взять список как ДАННЫЕ, не вычислять как вызов.
'(1 2 3) ; => (1 2 3)
'(+ 1 2) ; => (+ 1 2) — список из символа + и чисел, НЕ 3
; car/cdr — голова и хвост (исторические имена).
(car '(1 2 3)) ; => 1 (первый элемент)
(cdr '(1 2 3)) ; => (2 3) (всё кроме первого)
(first '(1 2 3)) ; => 1 (синоним car, читабельнее)
(rest '(1 2 3)) ; => (2 3) (синоним cdr)
(nth 2 '(a b c d)) ; => c (элемент по индексу, с нуля)
; Работа со списками.
(append '(1 2) '(3 4)) ; => (1 2 3 4)
(reverse '(1 2 3)) ; => (3 2 1)
(length '(1 2 3)) ; => 3
(member 2 '(1 2 3)) ; => (2 3) (хвост от найденного, иначе nil)
; ` (квазиквотирование) с , — подставить вычисленное значение в шаблон.
(let ((x 10))
`(значение ,x конец)) ; => (ЗНАЧЕНИЕ 10 КОНЕЦ)
Функции
; defun — определить именованную функцию. Последнее выражение — возврат.
(defun square (x)
(* x x))
(square 5) ; => 25
; lambda — анонимная функция.
((lambda (x) (* x x)) 7) ; => 49
(funcall (lambda (a b) (+ a b)) 3 4) ; => 7
; &optional — необязательные аргументы (со значением по умолчанию).
(defun greet (name &optional (greeting "Привет"))
(format nil "~a, ~a!" greeting name))
(greet "Аня") ; => "Привет, Аня!"
(greet "Аня" "Здорово") ; => "Здорово, Аня!"
; &rest — собрать остаток аргументов в список.
(defun my-sum (&rest numbers)
(reduce #'+ numbers))
(my-sum 1 2 3 4) ; => 10
; &key — именованные аргументы (порядок не важен).
(defun make-point (&key (x 0) (y 0))
(list x y))
(make-point :y 5 :x 3) ; => (3 5)
Условия
; if — ровно две ветки: (if УСЛОВИЕ тогда иначе).
(if (> 5 3) "больше" "меньше") ; => "больше"
; when — выполнить тело, ЕСЛИ условие истинно (без ветки «иначе»).
(when (> 5 3)
(format t "да~%")
:ok) ; => :OK
; unless — выполнить тело, ЕСЛИ условие ЛОЖНО.
(unless (> 1 5)
(format t "единица не больше пяти~%"))
; cond — цепочка условий (как else-if). Первое истинное срабатывает.
(defun classify (n)
(cond ((< n 0) :negative)
((= n 0) :zero)
(t :positive))) ; t — ветка «иначе»
(classify -3) ; => :NEGATIVE
; case — сравнение значения с вариантами (как switch).
(defun day-type (day)
(case day
((:sat :sun) :weekend)
(otherwise :workday)))
(day-type :sat) ; => :WEEKEND
Циклы
; dotimes — повторить N раз (i от 0 до N-1).
(dotimes (i 3)
(format t "i = ~a~%" i)) ; печатает i = 0, i = 1, i = 2
; dolist — перебрать элементы списка.
(dolist (item '(a b c))
(format t "~a~%" item)) ; печатает a, b, c
; loop — мощнейший макрос-цикл с собственным мини-языком.
(loop for i from 1 to 5
collect (* i i)) ; => (1 4 9 16 25)
(loop for x in '(1 2 3 4 5 6)
when (evenp x)
sum x) ; => 12 (сумма чётных)
(loop for i from 0 below 3
do (format t "шаг ~a~%" i))
; Простой бесконечный loop с явным выходом.
(let ((n 0))
(loop
(incf n) ; incf — увеличить на 1
(when (>= n 3) (return n)))) ; => 3
Функции высшего порядка
#' — синтаксис для ссылки на функцию (function quote).
; mapcar — применить функцию к каждому элементу, собрать результаты.
(mapcar #'square '(1 2 3 4)) ; => (1 4 9 16)
(mapcar #'+ '(1 2 3) '(10 20 30)) ; => (11 22 33) (параллельно)
; reduce — свернуть список одним значением.
(reduce #'+ '(1 2 3 4)) ; => 10
(reduce #'* '(1 2 3 4 5)) ; => 120
(reduce #'max '(3 7 2 9 4)) ; => 9
; remove-if / remove-if-not — фильтрация.
(remove-if #'evenp '(1 2 3 4 5 6)) ; => (1 3 5) (убрать чётные)
(remove-if-not #'evenp '(1 2 3 4 5 6)) ; => (2 4 6) (оставить чётные)
; С lambda — на лету.
(mapcar (lambda (x) (* x 10)) '(1 2 3)) ; => (10 20 30)
(remove-if (lambda (x) (> x 3)) '(1 2 3 4 5)) ; => (1 2 3)
; sort, find, count — тоже принимают предикаты.
(sort (list 3 1 2) #'<) ; => (1 2 3)
(count-if #'oddp '(1 2 3 4 5)) ; => 3
Макросы — фишка Lisp
Макрос получает код как данные (списки) и возвращает новый код ДО его выполнения. Так в язык добавляют новые конструкции.
; defmacro — определить макрос. ` — шаблон кода, , — подставить, ,@ — «расклеить».
(defmacro my-unless (condition &body body)
`(if ,condition
nil
(progn ,@body))) ; ,@ вставляет элементы списка body по очереди
(my-unless nil
(format t "выполнится~%")
42) ; => 42
; Макрос видит невычисленный код, поэтому может управлять его выполнением.
(defmacro swap (a b)
`(let ((tmp ,a))
(setf ,a ,b)
(setf ,b tmp)))
(let ((x 1) (y 2))
(swap x y)
(list x y)) ; => (2 1)
; macroexpand-1 показывает, во что разворачивается макрос — для отладки.
(macroexpand-1 '(my-unless nil (print 1)))
; => (IF NIL NIL (PROGN (PRINT 1)))
Структуры и хеш-таблицы
; defstruct — запись с полями. Автоматически создаёт make-, поле-аксессоры и предикат.
(defstruct person
name
(age 0)) ; age по умолчанию 0
(defparameter *p* (make-person :name "Аня" :age 30))
(person-name *p*) ; => "Аня" (аксессор)
(setf (person-age *p*) 31) ; изменение поля через setf
(person-p *p*) ; => T (предикат типа)
; Хеш-таблицы — словари ключ-значение.
(defparameter *h* (make-hash-table :test 'equal))
(setf (gethash "one" *h*) 1) ; положить
(setf (gethash "two" *h*) 2)
(gethash "one" *h*) ; => 1, T (значение и флаг «найдено»)
(gethash "zero" *h* :default) ; => :DEFAULT (если ключа нет)
; Обход хеш-таблицы.
(maphash (lambda (k v) (format t "~a => ~a~%" k v)) *h*)
Обобщённые функции и CLOS
CLOS (Common Lisp Object System) — встроенная объектная система. Методы выбираются по типам аргументов (множественная диспетчеризация).
; defclass — класс. :initarg — имя для конструктора, :accessor — геттер/сеттер.
(defclass animal ()
((name :initarg :name :accessor animal-name)
(legs :initarg :legs :accessor animal-legs :initform 4)))
; make-instance создаёт объект.
(defparameter *dog*
(make-instance 'animal :name "Рекс"))
(animal-name *dog*) ; => "Рекс"
(animal-legs *dog*) ; => 4 (из :initform)
; defmethod — метод обобщённой функции; диспетчеризация по типу аргумента.
(defmethod speak ((a animal))
(format nil "~a издаёт звук" (animal-name a)))
; Наследование и переопределение метода для подкласса.
(defclass dog (animal) ())
(defmethod speak ((d dog))
(format nil "~a говорит: Гав!" (animal-name d)))
(speak (make-instance 'dog :name "Шарик")) ; => "Шарик говорит: Гав!"
Особенности языка
; ГОМОИКОННОСТЬ: код Lisp — это сами списки Lisp. Поэтому программу можно
; конструировать и преобразовывать как обычные данные — отсюда сила макросов.
(defparameter *code* '(+ 1 2 3))
(eval *code*) ; => 6 (выполнить список как код)
(car *code*) ; => + (а можно работать как с данными)
; REPL (Read-Eval-Print-Loop): интерактивная разработка — определяешь и
; переопределяешь функции на лету, не перезапуская программу.
; В терминале SBCL/CCL приглашение выглядит так:
; CL-USER> (defun double (x) (* x 2))
; DOUBLE
; CL-USER> (double 21)
; 42
; Несколько возвращаемых значений (без обёрток-кортежей).
(floor 7 2) ; => 2 и 1 (частное и остаток)
(multiple-value-bind (q r) (floor 7 2)
(list q r)) ; => (2 1)
; Реализации: SBCL (самая популярная, компилятор в нативный код),
; CCL, ECL, ABCL (на JVM). Менеджер библиотек — Quicklisp.