--- language: "Common Lisp" filename: commonlisp.lisp contributors: - ["Paul Nathan", "https://github.com/pnathan"] - ["Rommel Martinez", "https://ebzzry.io"] translators: - ["Michael Filonenko", "https://github.com/filonenko-mikhail"] lang: ru-ru --- Common Lisp - мультипарадигменный язык программирования общего назначения, подходящий для широкого спектра задач. Его частенько называют программируемым языком программирования. Идеальная отправная точка - книга [Common Lisp на практике (перевод)](http://lisper.ru/pcl/). Ещё одна популярная книга [Land of Lisp](http://landoflisp.com/). И одна из последних книг [Common Lisp Recipes](http://weitz.de/cl-recipes/) вобрала в себя лучшие архитектурные решения на основе опыта коммерческой работки автора. ```common-lisp ;;;----------------------------------------------------------------------------- ;;; 0. Синтаксис ;;;----------------------------------------------------------------------------- ;;; Основные формы ;;; Существует два фундамента CL: АТОМ и S-выражение. ;;; Как правило, сгруппированные S-выражения называют `формами`. 10 ; атом; вычисляется в самого себя :thing ; другой атом; вычисляется в символ :thing t ; ещё один атом, обозначает `истину` (true) (+ 1 2 3 4) ; s-выражение '(4 :foo t) ; ещё одно s-выражение ;;; Комментарии ;;; Однострочные комментарии начинаются точкой с запятой. Четыре знака подряд ;;; используют для комментария всего файла, три для раздела, два для текущего ;;; определения; один для текущей строки. Например: ;;;; life.lisp ;;; То-сё - пятое-десятое. Оптимизировано для максимального бадабума и ччччч. ;;; Требуется для функции PoschitatBenzinIsRossiiVBelarus (defun meaning (life) "Возвращает смысл Жизни" (let ((meh "abc")) ;; Вызывает бадабум (loop :for x :across meh :collect x))) ; сохранить значения в x, и потом вернуть ;;; А вот целый блок комментария можно использовать как угодно. ;;; Для него используются #| и |# #| Целый блок комментария, который размазан на несколько строк #| которые могут быть вложенными! |# |# ;;; Чем пользоваться ;;; Существует несколько реализаций: и коммерческих, и открытых. ;;; Все они максимально соответствуют стандарту языка. ;;; SBCL, например, добротен. А за дополнительными библиотеками ;;; нужно ходить в Quicklisp ;;; Обычно разработка ведется в текстовом редакторе с запущенным в цикле ;;; интерпретатором (в CL это Read Eval Print Loop). Этот цикл (REPL) ;;; позволяет интерактивно выполнять части программы вживую сразу наблюдая ;;; результат. ;;;----------------------------------------------------------------------------- ;;; 1. Базовые типы и операторы ;;;----------------------------------------------------------------------------- ;;; Символы 'foo ; => FOO Символы автоматически приводятся к верхнему регистру. ;;; INTERN создаёт символ из строки. (intern "AAAA") ; => AAAA (intern "aaa") ; => |aaa| ;;; Числа 9999999999999999999999 ; целые #b111 ; двоичные => 7 #o111 ; восьмеричные => 73 #x111 ; шестнадцатиричные => 273 3.14159s0 ; с плавающей точкой 3.14159d0 ; с плавающей точкой с двойной точностью 1/2 ; рациональные) #C(1 2) ; комплексные ;;; Вызов функции пишется как s-выражение (f x y z ....), где f это функция, ;;; x, y, z, ... аругменты. (+ 1 2) ; => 3 ;;; Если вы хотите просто представить код как данные, воспользуйтесь формой QUOTE ;;; Она не вычисляет аргументы, а возвращает их как есть. ;;; Она даёт начало метапрограммированию (quote (+ 1 2)) ; => (+ 1 2) (quote a) ; => A ;;; QUOTE можно сокращенно записать знаком ' '(+ 1 2) ; => (+ 1 2) 'a ; => A ;;; Арифметические операции (+ 1 1) ; => 2 (- 8 1) ; => 7 (* 10 2) ; => 20 (expt 2 3) ; => 8 (mod 5 2) ; => 1 (/ 35 5) ; => 7 (/ 1 3) ; => 1/3 (+ #C(1 2) #C(6 -4)) ; => #C(7 -2) ;;; Булевые t ; истина; любое не-NIL значение `истинно` nil ; ложь; а ещё пустой список () тоже `ложь` (not nil) ; => T (and 0 t) ; => T (or 0 nil) ; => 0 ;;; Строковые символы #\A ; => #\A #\λ ; => #\GREEK_SMALL_LETTER_LAMDA #\u03BB ; => #\GREEK_SMALL_LETTER_LAMDA ;;; Строки это фиксированные массивы символов "Hello, world!" "Тимур \"Каштан\" Бадтрудинов" ; экранировать двойную кавычку обратным слешом ;;; Строки можно соединять (concatenate 'string "ПРивет, " "мир!") ; => "ПРивет, мир!" ;;; Можно пройтись по строке как по массиву символов (elt "Apple" 0) ; => #\A ;;; Для форматированного вывода используется FORMAT. Он умеет выводить, как просто значения, ;;; так и производить циклы и учитывать условия. Первый агрумент указывает куда отправить ;;; результат. Если NIL, FORMAT вернет результат как строку, если T результат отправиться ;;; консоль вывода а форма вернет NIL. (format nil "~A, ~A!" "Привет" "мир") ; => "Привет, мир!" (format t "~A, ~A!" "Привет" "мир") ; => NIL ;;;----------------------------------------------------------------------------- ;;; 2. Переменные ;;;----------------------------------------------------------------------------- ;;; С помощью DEFVAR и DEFPARAMETER вы можете создать глобальную (динамческой видимости) ;;; переменную. ;;; Имя переменной может состоять из любых символов кроме: ()",'`;#|\ ;;; Разница между DEFVAR и DEFPARAMETER в том, что повторное выполнение DEFVAR ;;; переменную не поменяет. А вот DEFPARAMETER меняет переменную при каждом вызове. ;;; Обычно глобальные (динамически видимые) переменные содержат звездочки в имени. (defparameter *some-var* 5) *some-var* ; => 5 ;;; Можете использовать unicode. (defparameter *КУКУ* nil) ;;; Доступ к необъявленной переменной - это непредсказуемое поведение. Не делайте так. ;;; С помощью LET можете сделать локальное связывание. ;;; В следующем куске кода, `я` связывается с "танцую с тобой" только ;;; внутри формы (let ...). LET всегда возвращает значение последней формы. (let ((я "танцую с тобой")) я) ; => "танцую с тобой" ;;;-----------------------------------------------------------------------------; ;;; 3. Структуры и коллекции ;;;-----------------------------------------------------------------------------; ;;; Структуры (defstruct dog name breed age) (defparameter *rover* (make-dog :name "rover" :breed "collie" :age 5)) *rover* ; => #S(DOG :NAME "rover" :BREED "collie" :AGE 5) (dog-p *rover*) ; => T (dog-name *rover*) ; => "rover" ;;; DEFSTRUCT автоматически создала DOG-P, MAKE-DOG, и DOG-NAME ;;; Пары (cons-ячейки) ;;; CONS создаёт пары. CAR и CDR возвращают начало и конец CONS-пары. (cons 'SUBJECT 'VERB) ; => '(SUBJECT . VERB) (car (cons 'SUBJECT 'VERB)) ; => SUBJECT (cdr (cons 'SUBJECT 'VERB)) ; => VERB ;;; Списки ;;; Списки это связанные CONS-пары, в конце самой последней из которых стоит NIL ;;; (или '() ). (cons 1 (cons 2 (cons 3 nil))) ; => '(1 2 3) ;;; Списки с произвольным количеством элементов удобно создавать с помощью LIST (list 1 2 3) ; => '(1 2 3) ;;; Если первый аргумент для CONS это атом и второй аргумент список, CONS ;;; возвращает новую CONS-пару, которая представляет собой список (cons 4 '(1 2 3)) ; => '(4 1 2 3) ;;; Чтобы объединить списки, используйте APPEND (append '(1 2) '(3 4)) ; => '(1 2 3 4) ;;; Или CONCATENATE (concatenate 'list '(1 2) '(3 4)) ; => '(1 2 3 4) ;;; Списки это самый используемый элемент языка. Поэтому с ними можно делать ;;; многие вещи. Вот несколько примеров: (mapcar #'1+ '(1 2 3)) ; => '(2 3 4) (mapcar #'+ '(1 2 3) '(10 20 30)) ; => '(11 22 33) (remove-if-not #'evenp '(1 2 3 4)) ; => '(2 4) (every #'evenp '(1 2 3 4)) ; => NIL (some #'oddp '(1 2 3 4)) ; => T (butlast '(subject verb object)) ; => (SUBJECT VERB) ;;; Вектора ;;; Вектора заданные прямо в коде - это массивы с фиксированной длинной. #(1 2 3) ; => #(1 2 3) ;;; Для соединения векторов используйте CONCATENATE (concatenate 'vector #(1 2 3) #(4 5 6)) ; => #(1 2 3 4 5 6) ;;; Массивы ;;; И вектора и строки это подмножества массивов. ;;; Двухмерные массивы (make-array (list 2 2)) ; => #2A((0 0) (0 0)) (make-array '(2 2)) ; => #2A((0 0) (0 0)) (make-array (list 2 2 2)) ; => #3A(((0 0) (0 0)) ((0 0) (0 0))) ;;; Внимание: значение по-умолчанию элемента массива зависит от реализации. ;;; Лучше явно указывайте: (make-array '(2) :initial-element 'unset) ; => #(UNSET UNSET) ;;; Для доступа к элементу в позиции 1, 1, 1: (aref (make-array (list 2 2 2)) 1 1 1) ; => 0 ;;; Вектора с изменяемой длиной ;;; Вектора с изменяемой длиной при выводе на консоль выглядят также, ;;; как и вектора, с константной длиной (defparameter *adjvec* (make-array '(3) :initial-contents '(1 2 3) :adjustable t :fill-pointer t)) *adjvec* ; => #(1 2 3) ;;; Добавление новых элементов (vector-push-extend 4 *adjvec*) ; => 3 *adjvec* ; => #(1 2 3 4) ;;; Множества, это просто списки: (set-difference '(1 2 3 4) '(4 5 6 7)) ; => (3 2 1) (intersection '(1 2 3 4) '(4 5 6 7)) ; => 4 (union '(1 2 3 4) '(4 5 6 7)) ; => (3 2 1 4 5 6 7) (adjoin 4 '(1 2 3 4)) ; => (1 2 3 4) ;;; Несмотря на все, для действительно больших объемов данных, вам нужно что-то ;;; лучше, чем просто связанные списки ;;; Словари представлены хеш таблицами. ;;; Создание хеш таблицы: (defparameter *m* (make-hash-table)) ;;; Установка пары ключ-значение (setf (gethash 'a *m*) 1) ;;; Возврат значения по ключу (gethash 'a *m*) ; => 1, T ;;; CL выражения умеют возвращать сразу несколько значений. (values 1 2) ; => 1, 2 ;;; которые могут быть распределены по переменным с помощью MULTIPLE-VALUE-BIND (multiple-value-bind (x y) (values 1 2) (list y x)) ; => '(2 1) ;;; GETHASH как раз та функция, которая возвращает несколько значений. Первое ;;; значение - это значение по ключу в хеш таблицу. Если ключ не был найден, ;;; возвращает NIL. ;;; Второе возвращаемое значение, указывает был ли ключ в хеш таблице. Если ключа ;;; не было, то возвращает NIL. Таким образом можно проверить, это значение ;;; NIL, или ключа просто не было. ;;; Вот возврат значений, в случае когда ключа в хеш таблице не было: (gethash 'd *m*) ;=> NIL, NIL ;;; Можете задать значение по умолчанию. (gethash 'd *m* :not-found) ; => :NOT-FOUND ;;; Давайте обработаем возврат несколько значений. (multiple-value-bind (a b) (gethash 'd *m*) (list a b)) ; => (NIL NIL) (multiple-value-bind (a b) (gethash 'a *m*) (list a b)) ; => (1 T) ;;;----------------------------------------------------------------------------- ;;; 3. Функции ;;;----------------------------------------------------------------------------- ;;; Для создания анонимных функций используйте LAMBDA. Функций всегда возвращают ;;; значение последнего своего выражения. Как выглядит функция при выводе в консоль ;;; зависит от реализации. (lambda () "Привет Мир") ; => # ;;; Для вызова анонимной функции пользуйтесь FUNCALL (funcall (lambda () "Привет Мир")) ; => "Привет мир" (funcall #'+ 1 2 3) ; => 6 ;;; FUNCALL сработает и тогда, когда анонимная функция стоит в начале ;;; неэкранированного списка ((lambda () "Привет Мир")) ; => "Привет Мир" ((lambda (val) val) "Привет Мир") ; => "Привет Мир" ;;; FUNCALL используется, когда аргументы заранее известны. ;;; В противном случае используйте APPLY (apply #'+ '(1 2 3)) ; => 6 (apply (lambda () "Привет Мир") nil) ; => "Привет Мир" ;;; Для обычной функции с именем используйте DEFUN (defun hello-world () "Привет Мир") (hello-world) ; => "Привет Мир" ;;; Выше видно пустой список (), это место для определения аргументов (defun hello (name) (format nil "Hello, ~A" name)) (hello "Григорий") ; => "Привет, Григорий" ;;; Можно указать необязательные аргументы. По умолчанию они будут NIL (defun hello (name &optional from) (if from (format t "Приветствие для ~A от ~A" name from) (format t "Привет, ~A" name))) (hello "Георгия" "Василия") ; => Приветствие для Георгия от Василия ;;; Можно явно задать значения по умолчанию (defun hello (name &optional (from "Мира")) (format nil "Приветствие для ~A от ~A" name from)) (hello "Жоры") ; => Приветствие для Жоры от Мира (hello "Жоры" "альпаки") ; => Приветствие для Жоры от альпаки ;;; Можно также задать именованные параметры (defun generalized-greeter (name &key (from "Мира") (honorific "Господин")) (format t "Здравствуйте, ~A ~A, от ~A" honorific name from)) (generalized-greeter "Григорий") ; => Здравствуйте, Господин Григорий, от Мира (generalized-greeter "Григорий" :from "альпаки" :honorific "гражданин") ; => Здравствуйте, Гражданин Григорий, от альпаки ;;;----------------------------------------------------------------------------- ;;; 4. Равенство или эквивалентность ;;;----------------------------------------------------------------------------- ;;; У CL сложная система эквивалентности. Взглянем одним глазом. ;;; Для чисел используйте `=' (= 3 3.0) ; => T (= 2 1) ; => NIL ;;; Для идентичености объектов используйте EQL (eql 3 3) ; => T (eql 3 3.0) ; => NIL (eql (list 3) (list 3)) ; => NIL ;;; Для списков, строк, и битовых векторов - EQUAL (equal (list 'a 'b) (list 'a 'b)) ; => T (equal (list 'a 'b) (list 'b 'a)) ; => NIL ;;;----------------------------------------------------------------------------- ;;; 5. Циклы и ветвления ;;;----------------------------------------------------------------------------- ;;; Ветвления (if t ; проверямое значение "случилась истина" ; если, оно было истинно "случилась ложь") ; иначе, когда оно было ложно ; => "случилась истина" ;;; В форме ветвления if, все не-NIL значения это `истина` (member 'Groucho '(Harpo Groucho Zeppo)) ; => '(GROUCHO ZEPPO) (if (member 'Groucho '(Harpo Groucho Zeppo)) 'yep 'nope) ; => 'YEP ;;; COND это цепочка проверок для нахождения искомого (cond ((> 2 2) (error "мимо!")) ((< 2 2) (error "опять мимо!")) (t 'ok)) ; => 'OK ;;; TYPECASE выбирает ветку исходя из типа выражения (typecase 1 (string :string) (integer :int)) ; => :int ;;; Циклы ;;; С рекурсией (defun fact (n) (if (< n 2) 1 (* n (fact(- n 1))))) (fact 5) ; => 120 ;;; И без (defun fact (n) (loop :for result = 1 :then (* result i) :for i :from 2 :to n :finally (return result))) (fact 5) ; => 120 (loop :for x :across "abc" :collect x) ; => (#\a #\b #\c #\d) (dolist (i '(1 2 3 4)) (format t "~A" i)) ; => 1234 ;;;----------------------------------------------------------------------------- ;;; 6. Установка значений в переменные (и не только) ;;;----------------------------------------------------------------------------- ;;; Для присвоения переменной нового значения используйте SETF. Это уже было ;;; при работе с хеш таблицами. (let ((variable 10)) (setf variable 2)) ; => 2 ;;; Для функционального подхода в программировании, старайтесь избегать измений ;;; в переменных. ;;;----------------------------------------------------------------------------- ;;; 7. Классы и объекты ;;;----------------------------------------------------------------------------- ;;; Никаких больше животных в примерах. Берем устройства приводимые в движение ;;; мускульной силой человека. (defclass human-powered-conveyance () ((velocity :accessor velocity :initarg :velocity) (average-efficiency :accessor average-efficiency :initarg :average-efficiency)) (:documentation "Устройство движимое человеческой силой")) ;;; Аргументы DEFCLASS: ;;; 1. Имя класса ;;; 2. Список родительских классов ;;; 3. Список полей ;;; 4. Необязательная метаинформация ;;; Если родительские классы не заданы, используется "стандартный" класс ;;; Это можно *изменить*, но хорошенько подумайте прежде. Если все-таки ;;; решились вам поможет "Art of the Metaobject Protocol" (defclass bicycle (human-powered-conveyance) ((wheel-size :accessor wheel-size :initarg :wheel-size :documentation "Diameter of the wheel.") (height :accessor height :initarg :height))) (defclass recumbent (bicycle) ((chain-type :accessor chain-type :initarg :chain-type))) (defclass unicycle (human-powered-conveyance) nil) (defclass canoe (human-powered-conveyance) ((number-of-rowers :accessor number-of-rowers :initarg :number-of-rowers))) ;;; Если вызвать DESCRIBE для HUMAN-POWERED-CONVEYANCE то получите следующее: (describe 'human-powered-conveyance) ; COMMON-LISP-USER::HUMAN-POWERED-CONVEYANCE ; [symbol] ; ; HUMAN-POWERED-CONVEYANCE names the standard-class #: ; Documentation: ; A human powered conveyance ; Direct superclasses: STANDARD-OBJECT ; Direct subclasses: UNICYCLE, BICYCLE, CANOE ; Not yet finalized. ; Direct slots: ; VELOCITY ; Readers: VELOCITY ; Writers: (SETF VELOCITY) ; AVERAGE-EFFICIENCY ; Readers: AVERAGE-EFFICIENCY ; Writers: (SETF AVERAGE-EFFICIENCY) ;;; CL задизайнен как интерактивная система. В рантайме доступна информация о ;;; типе объектов. ;;; Давайте посчитаем расстояние, которое пройдет велосипед за один оборот колеса ;;; по формуле C = d * pi (defmethod circumference ((object bicycle)) (* pi (wheel-size object))) ;;; PI - это константа в CL ;;; Предположим мы нашли, что критерий эффективности логарифмически связан ;;; с гребцами каноэ. Тогда вычисление можем сделать сразу при инициализации. ;;; Инициализируем объект после его создания: (defmethod initialize-instance :after ((object canoe) &rest args) (setf (average-efficiency object) (log (1+ (number-of-rowers object))))) ;;; Давайте проверим что получилось с этой самой эффективностью... (average-efficiency (make-instance 'canoe :number-of-rowers 15)) ; => 2.7725887 ;;;----------------------------------------------------------------------------- ;;; 8. Макросы ;;;----------------------------------------------------------------------------- ;;; Макросы позволяют расширить синаксис языка. В CL нет например цикла WHILE, ;;; но его проще простого реализовать на макросах. Если мы отбросим наши ;;; ассемблерные (или алгольные) инстинкты, мы взлетим на крыльях: (defmacro while (condition &body body) "Пока `условие` истинно, выполняется `тело`. `Условие` проверяется перед каждым выполнением `тела`" (let ((block-name (gensym)) (done (gensym))) `(tagbody ,block-name (unless ,condition (go ,done)) (progn ,@body) (go ,block-name) ,done))) ;;; Взглянем на более высокоуровневую версию этого макроса: (defmacro while (condition &body body) "Пока `условие` истинно, выполняется `тело`. `Условие` проверяется перед каждым выполнением `тела`" `(loop while ,condition do (progn ,@body))) ;;; В современных комиляторах LOOP так же эффективен как и приведенный ;;; выше код. Поэтому используйте его, его проще читать. ;;; В макросах используются символы ```, `,` и `@`. ``` - это оператор ;;; квазиквотирования - это значит что форма исполнятся не будет, а вернется ;;; как данные. Оператаор `,` позволяет исполнить форму внутри ;;; квазиквотирования. Оператор `@` исполняет форму внутри квазиквотирования ;;; но полученный список вклеивает по месту. ;;; GENSYM создаёт уникальный символ, который гарантировано больше нигде в ;;; системе не используется. Так надо потому, что макросы разворачиваются ;;; во время компиляции и переменные объявленные в макросе могут совпасть ;;; по имени с переменными в обычном коде. ;;; Дальнйешую информацию о макросах ищите в книгах Practical Common Lisp ;;; и On Lisp ``` ## Для чтения На русском - [Practical Common Lisp](http://www.gigamonkeys.com/book/) На английском - [Practical Common Lisp](http://www.gigamonkeys.com/book/) - [Common Lisp: A Gentle Introduction to Symbolic Computation](https://www.cs.cmu.edu/~dst/LispBook/book.pdf) ## Дополнительная информация На русском - [Lisper.ru](http://lisper.ru/) На английском - [CLiki](http://www.cliki.net/) - [common-lisp.net](https://common-lisp.net/) - [Awesome Common Lisp](https://github.com/CodyReichert/awesome-cl) - [Lisp Lang](http://lisp-lang.org/) ## Благодарности в английской версии Спасибо людям из Scheme за отличную статью, взятую за основу для Common Lisp. - [Paul Khuong](https://github.com/pkhuong) за хорошую вычитку.