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.
Поддержать проект