learnxinyminutes-docs/ru/common-lisp.md
2024-12-08 23:20:53 -07:00

28 KiB
Raw Permalink Blame History

filename contributors translators
commonlisp.lisp
Paul Nathan
https://github.com/pnathan
Rommel Martinez
https://ebzzry.io
Michael Filonenko
https://github.com/filonenko-mikhail

Common Lisp - мультипарадигменный язык программирования общего назначения, подходящий для широкого спектра задач. Его частенько называют программируемым языком программирования.

Идеальная отправная точка - книга Common Lisp на практике (перевод). Ещё одна популярная книга Land of Lisp. И одна из последних книг Common Lisp Recipes вобрала в себя лучшие архитектурные решения на основе опыта коммерческой работки автора.

;;;-----------------------------------------------------------------------------
;;; 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 () "Привет Мир") ; => #<FUNCTION (LAMBDA ()) {1004E7818B}>

;;; Для вызова анонимной функции пользуйтесь 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 #<STANDARD-CLASS
;                                                    HUMAN-POWERED-CONVEYANCE>:
;  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

Для чтения

На русском

На английском

Дополнительная информация

На русском

На английском

Благодарности в английской версии

Спасибо людям из Scheme за отличную статью, взятую за основу для Common Lisp.