Skip to content

filonenko-mikhail/ub-lisp

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Краткий курс по Коммон Лиспу

Университет Буффало - Университет штата Нью-Йорк

Факультет компьютерных и инженерных наук

© 2004 Стюарт Шапиро, Дэвид Пирс. Все права защищены.

Ресурсы о Коммон Лиспе

Коммон Лисповая среда разработки

Для редактирования, тестирования и отладки:
  • Скачайте emacs для вашей операционной системы.
  • Установите реализацию Коммон Лиспа (sbcl, clozure cl)
  • Установите Quicklisp
  • Загрузите quicklisp-slime-helper следующей lisp формой: (ql:quickload :quicklisp-slime-helper)
  • Добавьте в файл ~/.emacs
    (load (expand-file-name "~/quicklisp/slime-helper.el"))
    ;; Замените "sbcl" на путь к реализации Коммон Лиспа
    (setq inferior-lisp-program "sbcl")
            
Для редактирования файлов:
  1. Используйте расширение .lisp
  2. Используйте режим Коммон Лиспа
  3. Учите команды этого режима с помощью C-h m
Для тестирования и отладки:
  1. M-x slime
  2. Это РЕПЛ – оболочка, в которой можно выполнять Лисповый код.
  3. Учите команды интерактивной Коммон Лисповой среды с помощью C-h m

Лисповый стиль программирования

  • Ориентирован на выражения: Последовательное вычисление выражений.
  • Можно считывать выражения из файла, можно их сразу писать в стандартный ввод.
  • Лисповая машина.
  • Цикл чтение-выполнение-вывод.

Числа

  • Числа являются атомами, которые вычисляются сами в себя.
  • Попробуйте цикл чтение-выполнение-вывод. Ну правда, всё просто:
    1. Чтение
    2. Создание объекта
    3. Выполнение объекта
    4. Выбор текстового представления объекта
    5. Вывод
  • Три сюрприза:
    1. большие числа (bignums)
    2. дроби (ratios)
    3. комплексные числа

Неатомные выражения

  • Кембриджская префиксная нотация

Выход: (exit) в Лисповом режиме.

Логические (булевы) значения

  • Лисповая Ложь это nil (пустой список) - атом, который вычисляется сам в себя. Попробуйте сами.
  • Лисповая Истина это t (символ t) - атом, который вычисляется сам в себя. Любой другой Лисповый объект кроме =nil=, также является истиной.
  • and и or являются Лисповыми макросами, которые принимают любое количество аргументов, и лениво их вычисляют. Попробуйте сами, указывая разное количество аргументов. Попробуйте вызвать их вообще без аргументов. Эти макросы возвращают t, nil или значение последнего вычисленного выражения.

    Упражнение: Создайте файл для Лиспового кода. Теперь в начале просто наберите несколько комментариев с указанием задач данного файла.

Комментарии

;В конце строкиВ строке после кода
;;Во всю строкуОтступ как у кода
;;;Во всю строкуВ начале строки
#\vert ... \vert#Скобки для многострочных комментариевДля комментирования блоков кода

Создание функций

  • Изучите раздел о макросе defun
  • Например
    (defun average (x y)
      "Возвращает среднее арифметическое для чисел x и y."
      ;; Не округляет и не сокращает целые числа
      (/ (+ x y) 2))
        
  • Переменные имеют лексическое пространство.
  • Тип имеют объекты, а не переменные.
  • Загрузите файл: (load "file-name") в РЕПЛе или C-c C-l в буфере с исходным кодом
  • Упраженение: Создайте (discrim a b c), которая возвращает квадратный корень выражения b2 - 4ac (discrim 2 7 5) должна вернуть 3.0
  • Сюрприз в том, что Лисповые функции могут возвращать несколько значений Попробуйте (floor 5.25) или (round 5.25)
  • Например
    (defun +- (x d)
      "Возвращает x+d и x-d."
      (values (+ x d)
          (- x d)))
        

    Попробуйте: (values)

  • Упражнение: Используя discrim, определите (quad-roots a b c) для возврата корней квадратного уравнения

    ax2 + bx + c = 0

    то есть, (-b + sqrt(b2 - 4ac))/2a и (-b - sqrt(b2 - 4ac))/2a (quad-roots 2 7 5) должна возвращать -1.0 и -2.5

Условное выполнение (две ветки)

(if test-form then-form [else-form])

Заметьте: if является специальной формой

Например:

(defun fact (n)
  "Возвращает факториал от n"
  (if (<= n 0)
      1
    (* n (fact (1- n)))))

Упражнение: Создайте (fibonacci n), которая возвращает n-ое число Фибоначи: 1 1 2 3 5 8 13 …

Трассировка

(trace function-name ... function-name) включает трассировку указанных функций. (trace) возвращает список трассируемых функций. (untrace function-name ... function-name) выключает трассировку указанных функций (untrace) выключает все трассировки.

Когда курсор находится на названии функции нажмите C-c t, и для этой функции включится трассировка.

Включите трассировку для функций discrim и quad-root и при их вызовах посмотрите, что будет. Затем выключите трассировку.

Строковые символы

  • Строковые символы, как и числа, это “атомы, которые вычисляются в себя”. Их синтаксис #/<имя символа>/. Попробуйте сами:
    #\a
    #\space
    #\newline
        
  • Lisp умеет Unicode, поэтому можно делать так.
    #\cyrillic_small_letter_a
    #\cyrillic_small_letter_je
    #\latin_small_letter_eth
    #\greek_capital_letter_sigma
        
  • Теперь выполните следующий код:
    (format t "~a" #\latin_small_letter_a_with_acute)
        

    Format это Лисповый эквивалент функции printf, только, конечно, (ГОРАЗДО!) более мощный. Мы поговорим подробнее о нём позже, но для начала, format t просто выводит результат в стандартный вывод, и ~a указывает, что напечатанный объект должен быть человекочитаемым.

    Lisp может выводить Unicode символы, но Emacs’у это сделать сложнее, поэтому можно вывести код символа с помощью char-code:

    (char-code #\greek_capital_letter_sigma)
        
  • Для сравнения строковых символов используйте char, char<, char>.

Строки

  • Строки также являются атомами, которые вычисляются в себя, и указываются как последовательность символов между двойными кавычками.
  • Создание строк:
    "вот строка"
    (char "вот строка" 0)
    (char "вот строка" 2)
    "строка с таким \" знаком"
    (char "строка с таким \" знаком" 11) 
    (char "строка с таким \" знаком" 12) 
    (char "строка с таким \" знаком" 13)
    (format t "~a" "строка с таким \" знаком")
    (string #\latin_small_letter_a_with_acute)
    (string-capitalize "дэвид.р.пирс")
    (string-trim "as" "sassafras")
        
  • Сравнение строк:
    (string= "дэвид пирс" "Дэвид Пирс")
    (string-equal "дэвид пирс" "Дэвид Пирс")
    (string< "Дэвид Пирс" "Стью Шапиро")
    (string/= "foobar" "foofoo")
        
  • Строки как последовательности:
    (length "просто строка")
    (length "\\")
    (format t "~a" "\\")
    (subseq "просто строка" 3)
    (subseq "просто строка" 3 6)
    (position #\space "просто строка")
    (position #\i "Дэвид Пирс")
    (position #\i "Дэвид Пирс" :start 5)
    (search "pi" "дэвид пирс и стью шапиро")
    (search "pi" "дэвид пирс и стью шапиро" :start2 10)
    (concatenate 'string "foo" "bar")
    (concatenate 'string
      "d" (string #\latin_small_letter_a_with_grave)
      "v" (string #\latin_small_letter_i_with_acute)
      "d")
        
  • Упражнение: Определите (string-1+ s), которая создаёт новую строку, прибавляя 1 к каждому коду символа старой строки. Например, =(string-1+ “a b c”) => “b!c!d”=.

Символы

  • Символ является атомом, который может иметь, а может и не иметь значение.
  • Синтаксис: почти любая последовательность строковых символов (в разных регистрах), которая не может быть числом. (Внимание: в некоторых старых реализациях Лиспа, считыватель возводит в верхний регистр все строковые символы, даже если они были экранированы.)
  • Экранирующий строковый символ: \
  • Экранирующие скобки: | ... |
  • Аттрибуты символа
    1. symbol-name
    2. symbol-value
    3. symbol-function
    4. symbol-package
    5. symbol-plist
  • Квотировние: ’expression всегда вычисляется в expression, а не в значение символа expression
  • Загрузите ваш файл с исходным кодом функции average Попробуйте следующие формы:
    (type-of 'average)
    (symbol-name 'average)
    (type-of (symbol-name 'average))
    (symbol-function 'average)
    #'average
    (type-of #'average)
    (type-of (type-of #'average))
    (function-lambda-expression #'average)
        
  • Поместите ваш курсор в буфер и нажмите C-x 1. Перейдите на слово average нажмите C-c C-d C-d.
  • Функция для проверки равенства символов: eql Попробуйте сами.
  • Как Лисповый считыватель узнаёт откуда символ, который вы только что напечатали?
    1. Считывает все напечатанные строковые символы, конструирует строку (имя символа).
    2. Ищет атом по имени в “каталоге” (возможно в хеш-таблице).
    3. Если его там нет, создаёт его, и туда кладёт.

    Процесс установки символа в каталог называется пакетированием, символ, который был инсталлирован – пакетным символом.

Пакеты

Пакет является каталогом (отображением) имя символа->символ, другими словами, “пространством имён”. Всегда имеется текущий пакет, который Лисповый считыватель использует для поиска имён символов. Попробуйте выполнить *package* в РЕПЛе.

Лисповые пакеты никак не связаны с директориями или файлами. Обычно каждый файл в свою очередь наполняет явно указанный пакет.

Пакетированный символ в пакете может быть внутренним или внешним, и данный пакет для символа рассматривается как домашний пакет. Найти домашний пакет для символа можно формой (symbol-package symbol) Попробуйте (symbol-package 'average) и (symbol-package 'length)

У каждого пакета есть имя, и также может быть один или несколько псевдонимов. Попробуйте: (package-name (symbol-package 'average)) и (package-nicknames (symbol-package 'average))

Связь между пакетами и их псевдонимами:

(find-package package-name-or-symbol) (package-name package) (package-nicknames package)

Выполните (describe 'average) Вы уже можете понять всё, что было получено этой формой.

Выполните (describe 'length) Обратите внимание сколько было получено пакетов.

Поместите курсор над символом или в РЕПЛе или в файле с Лисповым кодом, и нажмите C-c С-d С-d, затем RET в минибуфере.

Попробуйте (documentation 'average 'function)

Автодополнение символов: M-TAB

Вы можете сделать символ внешним в домашнем пакете с помощью формы export. Попробуйте (export 'average). А теперь опять (describe 'average).

Вы можете изменить пакет с помощью формы in-package. Попробуйте (in-package :common-lisp)

Вы можете сослаться на символ с домашним пакетом p из какого-либо другого пакета, вне зависимости от того является ли символ внешним. Для ссылки на внешний символ s из пакета p наберите p:s Для ссылки на внутренний символ s из пакета p наберите p::s

Попробуйте сами:

'cl-user::discrim
'cl-user::average
'cl-user:average
'cl-user::length
'discrim

Обратите внимание на текстовое представление, которое Lisp выбирает для этих символов. Обратите внимание, что последняя строка указывает Лиспу создать символ с именем =”discrim”= в пакете common-lisp.

Для перехода обратно в пакет common-lisp-user наберите: (in-package :common-lisp-user)

Попробуйте сами

'cl-user::discrim
'cl::discrim
(symbol-name 'discrim)
(symbol-name 'cl::discrim)
(string= (symbol-name 'discrim) (symbol-name 'cl::discrim))
(eql 'discrim 'cl::discrim)

Не смущайтесь того, что discrim и cl::discrim это разные символы, просто у них одинаковое имя.

Два специальных пакета

  1. Пакет ключевых символов

    Каждый символ в этом пакете является внешним и вычисляется сам в себя.

    Этот символ создаётся с помощью пустого имени пакета и одинарного двоеточия : Попробуйте (describe :foo)

  2. Непакет

    Если считыватель видит строку вида #:s, он создаёт беспакетный символ с именем =”s”=, то есть символ, у которого нет домашнего пакета. Беспакетный символ не может быть найден Лисповым считывателем, и таким образом беспакетные символы никогда не равны eql друг другу, даже если у них одинаковые имена.

    Попробуйте:

    (describe '#:foo)
    (eql '#:foo '#:foo)
    (string=  (symbol-name '#:foo) (symbol-name '#:foo))
        

    Выполните (gensym). gensym создаёт новые беспакетные символы.

Создание пакетов

Самый простой путь создания пакета это форма (defpackage package-name), где package-name, не вычисляется и должно быть строкой или символом (в последнем случае используется имя символа). Рекомендуется использовать ключевой символ, например, (defpackage :test).

Посмотрите на буфер в Emacs’е, в котором вы выполняли упражнения. В модлайне будет указан пакет для данного буфера.

Введите форму (defpackage :test) в самом начале файла, прямо сразу за комментариями.

Мы хотим, чтобы символы в этом файле были спакетированы в пакет test. Это значит надо изменить текущий пакет на test, чтобы считыватель ориентировался на него. Сразу после формы определения пакета выполните (in-package :test) . Макрос in-package принимает строку или символ. Мы рекомендуем использовать ключевой символ.

Когда Lisp загружает файл, он сохраняет, а затем восстанавливает *package*. Поэтому после загрузки файла вам не надо вызывать in-package для возврата в ваш пакет.

Вопрос: Находился ли Лисповый считыватель в пакете exercises при чтении форм в вашем файле?

Сделайте символы, определённые в вашем пакете exercises, внешними: Измените форму

=(defpackage :exercises)=

на

(defpackage :exercises
        (:export #:average #:discrim #:fact #:quad-roots #:string-1+))

Сохраните эту версию файла, перезагрузите Lisp, загрузите файл и попробуйте использовать функции уже из common-lisp-user пакета.

Использование пакетов

Пакет может использовать другой пакет. В этом случае, все внешние символы используемого пакеты в первом пакете будут доступны без указания родительского пакета.

Например, пакет common-lisp-user использует пакет common-lisp, поэтому мы можем вызвать функцию length без указания пакета common-lisp. Посмотреть на это глазами можно с помощью формы (package-use-list :user).

В РЕПЛе, в пакете user выполните форму (use-package :exercises). Теперь вызывайте функции без указания домашнего пакета.

Скрытие символов

Упражнение: В вашем файле, определите функцию last, которая принимает строку и возвращает её последний символ.

Вы не можете это сделать, потому что last это имя функции, которая определена в пакете common-lisp, вы не можете её переопределить.

В пакете common-lisp много символов. Должны ли вы избегать коллизий с ними всеми? Нет!

Измените текущий пакет в РЕПЛе на exercises, и скройте символ cl:last с помощью (shadow 'last), и затем наберите ваше определение функции в РЕПЛе. Проверьте результат.

Добавьте ваше определение last в ваш файл с исходным кодом, и добавьте форму (:shadow cl:last) в форму defpackage. Также добавьте символ last в список экспортируемых (внешних) символов.

Перезапустите Lisp, загрузите файл. Проверьте функцию last.

Попробуйте использовать пакет exercises в пакете user. Возникнет конфликт. Будет задан вопрос, о том, какой из символов cl:last или exercises:last нужно использовать.

Списки и Cons-ячейки

Список является фундаментальной структурой данных в Лиспе, от которой и получил своё название язык (LISt Processing).

Список является объектом, который хранит последовательность элементов, которые могут быть или ссылаться на Лисповые объекты. Синтаксис списков такой: (a b c …). Списки создаются с помощью формы list.

'()
'(1 2 3)
(list 1 2 3)

Заметьте, что Lisp выводит пустой список =’()= как nil. Символ nil помимо значения Ложь, означает пустой список.

Упражнение: Создайте список содержащий два списка (1 2 3) и (4 5 6).

Доступ к элементам:

(first '(1 2 3))
(second '(1 2 3))
(third '(1 2 3))
(nth 5 '(1 2 3 4 5 6 7 8 9 10))
(rest '(1 2 3))
(rest (rest '(1 2 3)))
(nthcdr 0 '(1 2 3 4 5 6 7 8 9 10))
(nthcdr 5 '(1 2 3 4 5 6 7 8 9 10))

Работа со списками:

(endp '())
(endp '(1 2 3))
(endp nil)
(endp ())
(listp '())
(listp '(1 2 3))
(eql '(1 2 3) '(1 2 3))
(equal '(1 2 3) '(1 2 3))
(length '(1 2 3))
(append '(1 2 3) '(4 5 6))
(member 3 '(1 2 3 4 5 6))
(last '(1 2 3 4 5 6))
(last '(1 2 3 4 5 6) 3)
(butlast '(1 2 3 4 5 6))
(butlast '(1 2 3 4 5 6) 3)

Списки также являются последовательностями.

Упражнение: Напишите функцию (reverse l), которая возвращает список, содержащий элементы списка l в обратном порядке. (Common Lisp уже содержит функцию с таким именем, поэтому вам нужно вновь разрешить конфликт имён.)

Базовый строительный объект списка называется “cons-ячейка”. Cons-ячейка это объект, которые содержит два элемента. Элементы называются car и cdr (по историческим причинам). Синтаксис cons-ячейки выглядит так:

(object1 . object2) 

Cons-ячейки обычно используются для создания (связного) списка.

(object1 . (object2 . (object3 . (object4 . nil))))

Когда мы используем cons-ячейки для построения списков, мы будет часто ссылаться на элементы как на первый и оставшийся, или как на головной и хвостовой. Список список, которого последний cdr элемент не nil, называется списком с точкой (например, (1 2 . 3)). “Правильный список” в последнем cdr содержит nil. Функция cons создаёт cons-ячейку. Так как списки состоят из cons-ячеек функция cons также используется для добавления элементов в начало списка.

Работа с cons-ячейками:

(cons 1 2)
(cons 1 nil)
'(1 . nil)
(cons 1 '(2 3))
(consp '(1 . 2))
(car '(1 . 2))
(cdr '(1 . 2))
(first '(1 . 2))
(rest '(1 . 2))

Между прочим, cons-ячейки могут использоваться для создания бинарных деревьев.

(root . ((child1 . leaf1) . (child2 . ((child3 . leaf3) leaf2))))

Упражнение: Создайте бинарное дерево как на картинке.

Упражнение: Определите функцию (flatten2 binary-tree), которая возвращает элементы дерева binary-tree.

Более того, правильные списки могут использоваться для создания деревьев с произвольным количеством дочерних узлов. Например, ((a (b) c) (d ((e)) () f)).

Условные переходы (одна ветка)

if может использоваться без else ветки. В этом случае, else ветка неявно возвращает nil. Однако лучше использовать формы when и unless. В частности (when test expression...), вычисляет test, и если условие истинно, вычисляет оставшиеся выражения, возвращая результат последнего, если условие ложно возвращает nil. Так же (unless test expression...) вычисляет выражения, если test ложно.

Между прочим, многие Лисповые формы принимают последовательность выражений и возвращают результат последнего из них. Сюда входят defun, when, unless и cond, который будут рассмотрены далее. Часто говорится, что такие формы содержат “неявный progn”.

Условные переходы с одной веткой полезны, в частности, тогда. когда по-умолчанию значение для вычисления nil. Например:

(defun member (x list)
  "Возвращает истину, если x содержится в списке list."
  (when list
    (or (eql x (first list)) (member x (rest list)))))

Упражнение: Напишите функцию (get-property x list), которая возвращает элемент список list сразу за элементом x, или nil, если x в списке list не содержится. Например, (get-property 'name '(name david office 125)) => david. (Для решения задачи может пригодится функция member, которая не просто возвращает t, когда находит x в списке. Вы можете также не использовать функцию when, но ради интереса, попробуйте и с ней.) Список такого вида, который используется в этой функции называется списком свойств. Существуют похожие встроенные функции getf и get-properties, они отличаются только порядком аргументов.

Условные переходы (несколько веток)

Форма многоветочного условного перехода выглядит так:

(cond
 (expression11 expression12 ...)
 (expression21 expression22 ...)
 ...
 (expressionn1 expressionn2 ...))

Выражение expressioni1 вычисляется начиная с i = 1 пока одно из них не возвратит не-=nil= значение. В этом случае вычисляется оставшаяся часть группы, и возвращается значение последнего выражения. Если все выражения expressioni1 вернули nil, тогда значение формы cond также nil. Часто встречается что значение всего выражения это значение последнего выполненного подвыражения.

Чаще всего, cond рассматривается так:

(cond
 (test1 expression1 ...)
 (test2 expression2 ...)
 ...
 (testn expressionn ...))

Последнее выражение test может быть t, тогда последняя ветка является веткой по-умолчанию.

(defun elt (list index)
  "Возвращает элемент списка в позиции /index/, или =nil=, если данной позиции не было."
  (cond
   ((endp list)
    nil)
   ((zerop index)
    (first list))
   (t
    (elt (rest list) (1- index)))))

Упражнение: Создайте функцию (flatten tree), которая принимает список, который представляет дерево, с произвольным количеством веток, и возвращает список, в котором перечислены все элементы дерева. Например: (flatten '((a (b) c) () (((d e))))) => (a b c d e).

Другим видом многоветочных условных выражений является форма case. Case выбирает ветку для исполнения в зависимости от значения заданного выражения (в других языках это называется “switch”). Например, представим, что попросили пользователя загадать число:

(case (read)
  (2 "прости друг, слишком мало")
  (3 "в яблочко!!")
  (4 "прости, слишком много")
  (t "сдался?!"))

Форму case можно примерно представить в виде формы cond.

(case expression
  (literal1 result1)
  (literal2 result2)
  ...
  (literaln resultn))

(cond
  ((eql 'literal1 expression) result1)
  ((eql 'literal2 expression) result2)
  ...
  ((eql 'literaln expression) resultn))

за исключением того, что expression вычисляется единожды. Как и в случае cond, последнее подвыражение может быть обозначено символом t, что сделает его, выражением по-умолчанию. Также заметьте, что в case форме ключ выражения не вычисляется, а следовательно его не нужно квотировать.

В отличие от сишного выражения switch, Лисповая case может иметь несколько ключей для одной ветки, без использования функционала break. Например,

(case (read)
  ((#\a #\e #\i #\o #\u) 'vowel)
  (#\y 'sometimes\ vowel)
  (t 'consonent))

Локальные переменные

Помните функцию quad-roots?

(defun quad-roots (a b c)
  "Возвращает корни квадратного уравнения ax^2 + bx + c."
  (values (/ (+ (- b) (discrim a b c)) (* 2 a))
      (/ (- (- b) (discrim a b c)) (* 2 a))))

Лучше было бы сэкономить время вычисления и сохранять промежуточные результаты в локальных переменных. Локальные переменные создаются с помощью формы let.

(defun quad-roots (a b c)
  "Возвращает корни квадратного уравнения ax^2 + bx + c."
  (let ((-b (- b))
        (d  (discrim a b c))
        (2a (* 2 a)))
    (values (/ (+ -b d) 2a) (/ (- -b d) 2a))))

Основной вид формы let:

(let ((v1 e1)
      (v2 e2)
      ...
      (vn en))
  expression
  ...)

Переменные с /v/1 по /v/n будут связаны с результатами вычислений выражений с /e/1 по /e/n. Эти связывания актуальны только для тела из выражений /expression/s. Как обычно результатом формы let является результат последнего выражения.

let связывания ограничены лексически:

(let ((x 1))
  (list
    (let ((x 2))
      x)
    (let ((x 3))
      x)))

let связывания выполняются параллельно:

(let ((x 3))
  (let ((x (1+ x))
        (y (1+ x)))
    (list x y)))

let* связывания выполняются последовательно:

(let ((x 3))
  (let* ((x (1+ x))
         (y (1+ x)))
    (list x y)))

Лябмда-списки.

Лямбда-списком называется список формальных параметров, которые перечислены после имени функции в форме defun. Лямбда-списки, которые мы видели раньше, содержат только обязательные параметры, но фактически они могут содержать пять видов параметров, перечисленных ниже.

Обязательные параметры
Обязательные параметры это обычные формальные параметры, к которым вы привыкли. Для каждого обязательного параметра может быть только один аргумент, и обязательные параметры связываются со значениями аргументов слева направо.
Необязательные параметры
Необязательные параметры следуют за ключевым символом &optional. Каждый необязательный параметр может выглядеть как:

var (var default-value) или (var default-value supplied-p)

Если переданных аргументов больше чем обязательных параметров, лишняя часть аргументов будет связана с необязательными параметрами слева направо. Если необязательные параметры ещё остались, они будут связаны со значениями default-value, если такие значения указаны, или с nil в противном случае. Если был указан supplied-p и при вызове был аргумент для параметра, то supplied-p будет t, иначе nil. Например:

  1. Заметьте, что функция last принимает необязательный аргумент.
  2. Попробуйте сами:
    (defun testOpt (a b &optional c (d 99 dSuppliedp))
      (list a b c d
            (if dSuppliedp '(supplied) '(default))))
    (testOpt 2 3)
    (testOpt 2 3 4 5)
            

Упражнение: Переопределите ваши reverse=/=reverse1 как одну функцию reverse, которая принимает один обязательный аргумент и один необязательный.

Оставшиеся параметры
При использовании только обязательных и необязательных аргументов Лисповая функция ограничивается максимальным количеством фактических аргументов. Если лямбда-список содержит ключевой символ &rest, то после него должен только один параметр, который при вызове будет содержать список всех значений фактических аргументов, которые были переданы после этого параметра.
  1. Заметьте, что функция - требует один обязательный параметр и оставшиеся параметры, так что функция принимает один или более аргументов.
  2. Заметьте, что функция and принимает оставшиеся параметры, то есть принимает ноль или более аргументов.
  3. Попробуйте сами
    (defun testRest (a b &rest c)
      (list a b c))
    (testRest 1 2)
    (testRest 1 2 3 4 5 6)
            

Упражнение: Функция union принимает два списка и возвращает список, который является объединением первых двух. Попробуйте сами. Создайте в своём пакете свою функцию union, которая принимает ноль и более аргументов в виде списков и используя cl:union верните объединение всех переданных списков.

Бонус: Лисповая функция apply принимает два аргумента: функцию и список аргументов для функции. apply возвращает значение выполненной функции с данными аргументами. Попробуйте сами:

(apply #'cons '(a b))
(apply #'+ '(1 2 3 4))
Именованные параметры
Проблема необязательных параметров в том, что если вы определили несколько необязательных аргументов, и пользователь хочет указать только второй из них, а первых оставить по-умолчанию, ему всё равно придётся указать первый аргумент. То есть первый фактический аргумент после обязательных аргументов, будет связан только с первым необязательным аргументом и никаким другим.

Именованные параметры являются необязательными, но их аргументы могут передаваться в любом порядке, и любой из них может быть указан или не указан вне зависимости от других.

Именованные параметры в лямбда-списке следуют за ключевым символом &key. Каждый ключевой символ может выглядеть как

var (var default-value) или (var default-value supplied-p)

Именованный параметр var используется в теле функции как обычно, но вот при вызове функции, именованный аргумент задаётся с помощью ключевого символа с тем же именем, что и параметр, то есть :var.

Упражнение:

  1. Попробуйте сами:
    (defun testKey (a &key oneKey (twoKey 99 2Suppliedp))
      (list a oneKey twoKey
        (if 2Suppliedp '(supplied) '(default))))
    (testKey 2)
    (testKey 2 :oneKey 5)
    (testKey 2 :twoKey 5)
    (testKey 2 :twoKey 10 :oneKey 5)
            
  2. Заметьте, что member имеет два обязательных параметра и три именованных. Попробуйте сами:
    (member '(a b) '((a c) (a b) (c a)))
    (member '(a b) '((a c) (a b) (c a)) :test #'equal)
    (member 'a '((a c) (a b) (c a)))
    (member 'a '((a c) (a b) (c a)) :key #'second)
    (member 'a '((a c) (a b) (c a)) :key #'second :test-not #'eql)
            
  3. Заметьте, что cl:union также принимает три именованных параметра. Измените лямбда-список вашей функции union так, чтобы она также принимала эти три параметра, и передайте эти аргументы в вызов cl:union.

    Бонус: Функция identity возвращает значение аргумента.

Вспомогательные параметры
Вспомогательные параметры в лямбда-списке следуют за ключевым символом &aux, и представляют списком локальных переменных с их значениями. Определение
(defun (var1 ... varn &aux avar1 ... avarm)
  body)
    

полностью эквивалентно выражению

(defun (var1 ... varn)
  (let* (avar1 ... avarm)
    body))
    

Упражнение:

  1. Попробуйте сами
    (defun test (x &aux (x (1+ x)) (y (1+ x)))
      (list x y))
    (test 3)
            
  2. Перепишите вашу функцию quad-roots с помощью вспомогательных параметров.

Итерация

В Лиспе есть несколько конструкций для создания циклов. Наиболее мощной и сложной является loop.

Простейший вид loop выглядит так: (loop expression...).

(loop for i from 1 to 10
  do (print (* i i)))

“Расширенный loop” содержит последовательность подвыражений. Вот простой пример

(loop for i from 1 to 10
  do (print (* i i)))

который содержит два подвыражения: (1) for i from 1 to 10 и (2) do (print (* i i)).

Как вы можете увидеть, loop не выглядит как обычный Lisp. В обычном Лиспе для структурирования программы используются списки. Loop синактически является более сложным, для структурирования используются “ключевые символы” (ключевые не в том смысле, что из пакета keyword). Каждый вид подвыражения обозначается отдельным символом, остальные же символы используются для внутренней структуры подвыражения.

Существует 7 подвыражений:

  1. Управление итерациями;
  2. Проверка завершения;
  3. Накопление значения;
  4. Безусловное выполнение подвыражения;
  5. Условное выполнение подвыражения;
  6. Первое-последнее подвыражение;
  7. Локальные переменные.
Управление итерациями

Управление итерациями включается символом for. Оно позволяет задать первоначальное и последнее значение, а также шаг для переменной. При достижении конечного значения цикл завершается. Управление итерациями содержит 7 подвидов. Некоторые из них перечисляют элементы структур данных, один подвид перечисляет числа, и один служит для обобщённых целей.

  1. Числовые интервалы: for var from start {to | upto | below | downto | above} end [by incr]
    (loop for i from 99 downto 66 by 3
      do (print i))
            
  2. Элементы списка: for var in list [by step-fun]
    (loop for x in '(a b c d e)
      do (print x))
    
    (loop for x in '(a b c d e) by #'cddr
      do (print x))
            

    Интересной особенностью является то, что можно использовать деструктуризацию.

    ;; Не обращайте внимание на =format=
    ;; Мы поговорим о нём позже
     (loop for (l n) in '((a 1) (b 2) (c 3) (d 4) (e 5))
       do (format t "~a is the ~:r letter~%" l n))
    
     (loop for (first . rest) in '((42) (a b) (1 2 3) (fee fie foe fum))
       do (format t "~3a has ~d friend~:*~p~%" first (length rest)))
            
  3. Подсписки списка: for var on list [by step-fun]
    (loop for x on '(a b c d e)
      do (print x))
    
    (loop for x on '(a b c d e) by #'cddr
      do (print x))
            

    И опять таки с деструктуризацией:

    (loop for (x y) on '(a b c d e f) by #'cddr
      do (print (list x y)))
            
  4. Элементы вектора: for var across vector
    (loop for c across "мама мыла раму"
      do (print (char-upcase c)))
            
  5. Элементы хеш-таблиц: for var being each {hash-key | hash-value} of hash-table
  6. Символы пакета: for var being each {present-symbol | symbol | external-symbol} [of package]
    (loop for x being each present-symbol of *package*
      do (print x))
            
  7. Что угодно for var = expression [then expression]
    (loop
      for x from 0 below 10
      for y = (+ (* 3 x x) (* 2 x) 1)
      do (print (list x y)))
    
    (loop
      for l in '(a b c d e)
      for m = 1 then (* 2 m)
      do (format t "битовая маска для ~a ~d~%" l m))
    
    (loop
      for prev = #\d then next
      for next across "avid"
      do (format t "~a стоит перед ~a~%" prev next))
            

Подвыражения в управление итерациями обычно выполняются последовательно. Вычисление шага может выполнятся параллельно, если использовать символ and.

Накопление значения

Обычно, loop возвращает =nil. Однако накопление значения может изменить это поведение.

Подвыражение для накопления значения в список выглядит так: {collect | append} expression [into var].

(defun explode (string)
  (loop for c across string collect c))

(defun flatten (tree)
  (if (listp tree)
    (loop for child in tree append (flatten child))
    (list tree)))

(loop for r on '(a b c d e)
  collect (length r)
  append r)
    

Подвыражение для накопления численного значения выглядит так: {count | sum | minimize | maximize} expression [into var].

(loop for l in '((1 2 3) () (fee fie foe fum) () (a b c d e))
  for n = (length l)
  count l into count
  sum n into sum
  minimize n into min
  maximize n into max
  do (print (list count sum min max)))

(loop for l in '((1 2 3) () (fee fie foe fum) () (a b c d e))
  for n = (length l)
  maximize n into max
  sum max)

(loop for l in '((1 2 3) () (fee fie foe fum) () (a b c d e))
  count l
  count l
  sum (length l))
    
Первые-последние подвыражения
(loop
  initially (format t "testing")
  repeat 10 do
  (sleep 0.5)
  (format t ".")
  finally (format t "done~%"))
    

Подвыражение finally особенно полезно при возврате значения, вычисленного в самом цикле.

(loop for l in '((1 2 3) () (fee fie foe fum) () (a b c d e))
  for n = (length l)
  count l into count
  sum n into sum
  minimize n into min
  maximize n into max
  finally (return (values count sum min max)))

;; just to mess with you
(loop repeat 5 collect (copy-list foo) into foo finally (return foo))
    

Упражнение: Перепишите функцию fact с использованием loop. Перепишите также функцию fibonacci.

Безусловное выполнение подвыражений

Вы уже видели два безусловных выполнения подвыражений

  • do expression ...
  • return expression

Только в подвыражениях do, initially и finally после ключевого слова допускается последовательность выражений для выполнения. Обычно они [выражения] выполняются последовательно.

Условное выполнение подвыражений

Форма условного выполнения подвыражений выглядит так

{if | when | unless} test
  selectable-clause {and selectable-clause}*  
[else
  selectable-clause {and selectable-clause}*]
[end]
    

где selectable-clause может быть: накоплением значения, безусловным выполнение подвыражения условным выполнением выражения.

(loop for x in '((1 2 3) 4 (5 6) 7 8)
  if (listp x)
    sum (apply #'* x)
  else
    sum x)
    

Упражнение: Перепишите функцию get-property с использованием loop. Объясните чем новая реализация лучше старой, принимая во внимание то, что нечётные элементы списка это ключи, а чётные - значения.

Проверка завершения
  • repeat number
  • while test
  • until test
  • always expression
  • never expression
  • thereis expression
(defun power (x n)
   (loop repeat n
     for y = x then (* y x)
     finally (return y)))

 (defun user-likes-lisp-p ()
   (loop initially (format t "Вы любите Lisp? ")
     for x = (read)
     until (member x '(д н))
     do (format t "Пожалуйста ответьте `д' или `н'. ")
     finally (return (eql x 'д))))

 (defun composite-p (n)
   (loop for k from 2 below (sqrt (1+ n))
     thereis (when (zerop (nth-value 1 (floor n k))) k)))

 ;; just for fun
 (defun prime-factorization (n)
   (let ((k (composite-p n)))
     (if k
       (append (prime-factorization k) (prime-factorization (floor n k)))
       (list n))))
    

Упражнение: Создайте функцию (split list splitters), которая возвращает список элементов списка list, которые заключены между элементами splitters. Например, (split '(1 2 3 4 5 6 7 8 9) '(3 6)) => '((1 2) (4 5) (7 8 9)). (Подсказка: используйте вложенные циклы.)

Существует ещё два способа остановить цикл. Форма (return [value]) немедленно останавливает цикл и возвращает value. Форма (loop-finish) останавливает цикл, вычисляя подвыражения finally, и возвращает все накопленные значения.

Циклу можно назначить имя – (loop named name clauses...). Из такого цикла можно выйти с помощью (return-from name [value]). (Если уточнить, то loop устанавливает неявный block с заданным именем, или с именем nil.)

Локальные переменные
(loop with s = "дэвид пирс"
  for prev = (char s 0) then next
  for next across (subseq s 1)
  do (format t "~a came before ~a~%" prev next))
    

Подвыражения with обычно инициализируются последовательно. Для параллельной инициализации необходимо использовать and.

Автор завершает данный урок дополнительными словами о циклах.

  • Как мы увидели, завершение цикла может произойти в нескольких местах – в управлении итерациями, в проверке завершения, и при использовании return и loop-finish. Цикл завершается при выполнении первого из этих выражений. В зависимости от завершения, цикл может вернуть или не вернуть значение, и выполнить или не выполнить последние выражения.
  • Кроме того loop достаточно гибкий в порядке расположения подвыражений. Главное правило в том, что выражения “для переменных” должны идти перед выражениями “для выполнения”. Выражения “для переменных” это управление итерациями и локальные переменные. Выражения “для выполнения” это выполнение, накопление значения и проверка завершения. Первые-последние выражения могут быть в любом месте.

Присваивание

Глобальные переменные

(defconstant name initial-value [documentation]) Невозможно изменить значение

(defparameter name initial-value [documentation])

(defvar name [initial-value [documentation]]) Невозможно переинициализировать переменную.

Стиль именования глобальных переменных *var*

Попробуйте сами:

(defconstant *Lab* 'Baldy\ 19
  "Где мы встречаемся.")
*Lab*
(defconstant *Lab* 'Baldy\ 21
  "Где мы встречаемся.")
*Lab*
(defparameter *Time* "TTh 1:30-2:30"
  "Время встречи")
*Time*
(defparameter *Time* "MTh 10:30-1:30"
  "Время встречи")
*Time*
(defvar *Attendance* 20
  "Количество студентов")
*Attendance*
(defvar *Attendance* 6
  "Количество студентов")
*Attendance*
    
Присваивание

(set symbol value) Выполняет оба аргумента.

(setq {symbol value}*) Не выполняет выражение symbol. Старый стиль.

(setf {place value}*) Использует первое значение выражения place. Последовательно.

(psetf {place value}*) Использует первое значение выражения place. Параллельно.

Попробуйте сами:

(setf *Lab* 'Baldy\ 19)
(setf *Time* "TTh 10:30-1:30"
      *Attendance* 10)
*Time*
*Attendance*

(setf x 3 y 5) ; Не присваивайте Don't assign to new global variables in a function body
x
y
(psetf x y y x)
x
y
    
Обобщённые переменные (места)
Обобщённая переменная может быть символом или же специальной формой, которая раскрываясь указывает на некоторую область, где можно сохранить объект. Например:
(setf x '(a b c d e))
(setf (second x) 2)
x

(setf addresses (make-hash-table))
(setf (gethash 'Stu addresses) 'shapiro@cse.buffalo.edu)
(setf (gethash 'David addresses) 'drpierce@cse.buffalo.edu)
(setf (gethash 'Luddite addresses) nil)
(gethash 'David addresses)
(gethash 'Stu addresses)
(gethash 'Luddite addresses)
(gethash 'Bill addresses)
    

Но будьте осторожны:

(defun goodTimers (folks)
   (append folks '(had a good time)))
(setf list1 (goodTimers '(Trupti Mike and Fran)))
(setf (seventh list1) 'bad)
list1
(goodTimers '(Jon Josephine and Orkan))
    

Некоторые полезные глобальные переменные

*

Последний объект возвращённый в РЕПЛе.

**

Предпоследний объект возвращённый в РЕПЛе.

***

Пред-предпоследний объект возвращённый в РЕПЛе.

*package*

Текущий пакет.

*print-base*

Основание системы счисления при выводе чисел.

*read-base*

Основание системы счисления при вводе чисел.

Упражнение: Превратите Лисповый РЕПЛ в конвертер из шестнадцатеричной системы счисления в двоичную. А затем наоборот.

Последовательное выполнение

Сейчас, когда мы рассмотрели присваивание, мы может рассмотреть другую императивную конструкцию – последовательное выполнение. Здесь нет ничего нового, потому что многие Лисповые формы позволяют выполнять последовательности выражений в “теле” формы. Например, это формы defun, cond и let.

Вспомните, что мы называли последовательность выражений в “теле” как неявный progn. Это потому, что неявный progn является Лисповой формой, для создания явной последовательности выражений. Результатом формы progn является значение последнего выражения. Значения всех остальных выражений игнорируются.

Обычно использовать progn нет необходимости, так как большинство конструкций создают неявный progn. Однако существует набор интересных вариаций progn, которые иногда бывают удобны: prog1 и prog2.

(prog1 1 2 3)
(prog2 1 2 3)
(progn 1 2 3)

Функции

Мы уже знаем кое-что о функциях – как минимум, об именованных функция.

  • Именованные функции создаются с помощью формы defun.
  • Функции вызываются с помощью выполнения списка, в котором в первом элементе указано имя функции – (function-name argument ...).
  • Форма (function function-name) может использоваться для получения объекта функции, имея только имя. Выражение #'function-name является аббревиатурой для (function function-name).

Что в Лиспе мы можем сделать с объектами функции?

  • Функции могут быть присвоены переменными, переданы как аргументы, и сохранены в структурах данных, просто как любые другие Лисповые объекты. Функции с такими свойствами, часто называются “функции высшего порядка”.
  • Функции могут применяться к аргументам argument/1 … /argument/n с помощью формы (funcall function argument1 ... argumentn).
  • Функции также могут применяться к аргументам с помощью формы (apply function argument1 ... argumentm-1 argumentsm...n), где arguments/m…n является списком аргументов от /m до n.

Некоторые примеры, которые мы уже видели:

(member '(a c) '((a b) (a c) (b c)) :test #'equal)

(loop for x in '(a b c d e) by #'cddr do (print x))

Парочка новых:

(funcall #'cons nil nil)

(setf some-functions (list #'third #'first #'second))

(funcall (first some-functions) '(a b c))

(defun multicall (list-of-functions &rest arguments)
  "Returns a list of results obtained by calling each function
in LIST-OF-FUNCTIONS on the ARGUMENTS."
  (loop for f in list-of-functions
    collect (apply f arguments)))

(multicall (list #'third #'second #'first) '(a b c))

Упражнение: Определите функцию (tree-member item tree &key (key #'identity) (test #'eql)), которая возвращает поддерево дерева tree с отметками и с корнем item, также как member работает для списков. Дерево с отметкой выглядит так (label . children), где children является списком дочерних элементов. Листья не имеют дочерних элементов. item эквивалентно отметке дерева tree, если (test item (key label)) истина. Например:

(tree-member "feline"
  '("animal"
    ("mammal"
     ("feline" ("lion") ("tiger") ("kitty"))
     ("rodent" ("squirrel") ("bunny") ("beaver")))
    ("bird" ("canary") ("pigeon"))
    ("reptile" ("turtle") ("snake")))
  :test #'string=)
==> ("feline" ("lion") ("tiger") ("kitty"))

Так как объекты функции могут так гибко использоваться, значит возможно, что мы можем создать функцию не задавая для неё имени. И ведь да, это делается с помощью формы lambda. Лямбда-выражение может быть использовано вместо имени функции.

#'(lambda (x) (+ x 1))

((lambda (x) (+ x 1)) 42)

(funcall #'(lambda (x) (+ x 1)) 42)

Следует отметить, что

((lambda lambda-list . body) . arguments) == (funcall #'(lambda lambda-list . body) . arguments).

А фактически форма function не является необходимой, потому что lambda сделана так, что:

(lambda lambda-list . body) == #'(lambda lambda-list . body).

Лямбда-функции также являются замыканиями, что означает, что в них хранится не только их код, но и также лексическое окружение. Таким образом они запоминают связывания переменных, сделанные во время создания этой лямбда-функции.

(defun make-adder (delta)
  (lambda (x) (+ x delta)))

(setf f (make-adder 13))
(funcall f 42)

(funcall (make-adder 11) (funcall (make-adder 22) 33))

Упражнение: Определите функцию (compose f g), которая компонует функции f и g. Допустим, что компоновка f с g выглядит как (f • g)(x) = f/(/g/(/x)). Попробуйте (funcall (compose #'char-upcase #'code-char) 100).

Отображение

Частенько бывает нужно применить функцию к каждому элементу списка и получить результаты каждого вызова. Эта операция называется отображение. Лямбда-функции в этом смысле очень удобны.

(mapcar #'(lambda (s) (string-capitalize (string s))) '(fee fie foe fum))

(maplist #'reverse '(a b c d e))

(mapcar #'(lambda (s n) (make-list n :initial-element s))
    '(a b c d e) '(5 2 3 7 11))

(mapcan #'(lambda (s n) (make-list n :initial-element s))
    '(a b c d e) '(5 2 3 7 11))

(mapcon #'reverse '(a b c d e))

Последовательности

Последовательности – это общий суперкласс (родительский класс) для списком и векторов (то есть одномерных массивов), или одномерные упорядоченные коллекции объектов. Последовательности также поддерживают отображения.

(map 'list #'(lambda (c) (position c "0123456789ABCDEF")) "2BAD4BEEF")

(map 'string #'(lambda (a b) (if (char< a b) a b))
     "Дэвид Пирс" "Стью Шапиро")

Вот ещё примерчик полезных функций для последовательностей. Многие из них принимают функции в качестве аргументов.

(count-if #'oddp '(2 11 10 13 4 11 14 14 15) :end 5)

(setf x "Дэвид Пирс")
(sort x #'(lambda (c d)
        (let ((m (char-code c)) (n (char-code d)))
          (if (oddp m)
                (if (oddp n) (< m n) t)
            (if (oddp n) nil (< m n))))))
;; заметьте, что SORT деструктивен
x

(find-if
 #'(lambda (c) (= (* (first c) (first c)) (second c)))
 '((1 3) (3 5) (5 7) (7 9) (2 4) (4 6) (6 8)))

(position-if
 #'(lambda (c) (= (* (first c) (first c)) (second c)))
 '((1 3) (3 5) (5 7) (7 9) (2 4) (4 6) (6 8)))

(reduce #'+ '(1 2 3 4))
(reduce #'list '(a b c d e))
(reduce #'list '(a b c d e) :initial-value 'z)
(reduce #'list '(a b c d e) :from-end t)
(reduce #'append '((a b) (c d) (e f g) (h) (i j k)))

Упражнение: Представьте, что вы получили список заголовков для столбцов таблицы – например, ("Function " "Arguments " "Return values " "Author " "Version "). Размер столбцов вычисляется с помощью длин этих заголовков. Напишите, выражение, которые вычисляет количество пробелов (или количество места) для вставки в n-нный столбец таблицы.

Ввод/Вывод

Ввод/вывод (чтение/запись) в Лиспе основан на потоках. Поток это источник или приёмник строковых символов или байтов. Например, поток может быть направлен в или из файла, строки или терминала. Поток в качестве необязательного аргумента принимают функции вывода (записи) (например, format и print) и функции ввода (чтения) (например, read). При запуске Лиспа доступны несколько стандартных потоков, включая *standard-input*, *standard-output*. Если сессия интерактивна, они оба являются синонимами для *terminal-io*.

Основными функциями вывода (записи) являются write-char и write-line. Основными функциями ввода (чтения) являются read-char и read-line.

Файловые потоки создаются с помощью функции open. Однако, удобнее использовать форму with-open-file, которая обязательно закроет файл в конце вне зависимости от того, возникла ли ошибка или нет в процессе работы с ним.

(with-open-file (output-stream "/tmp/drpierce.txt" ; укажите здесь своё имя
                 :direction :output)
  (write-line "Я люблю Lisp" output-stream))

(with-open-file (input-stream "/tmp/drpierce.txt" :direction :input)
  (read-line input-stream))

(with-open-file (output-stream "/tmp/drpierce.txt" 
                 :direction :output
                 :if-exists :supersede)
  (write-line "1. Lisp" output-stream))

(with-open-file (output-stream "/tmp/drpierce.txt" 
                 :direction :output
                 :if-exists :append)
  (write-line "2. Prolog" output-stream)
  (write-line "3. Java" output-stream)
  (write-line "4. C" output-stream))

;; чтение строк до конца файла
(with-open-file (input-stream "/tmp/drpierce.txt" :direction :input)
  (loop for line = (read-line input-stream nil nil)
    while line
    collect line))

Подобным образом, строковый поток обычно управляется с помощью with-output-to-string и with-input-from-string.

(with-output-to-string (output-stream)
  (loop for c in '(#\L #\i #\s #\p)
    do (write-char c output-stream)))

(with-input-from-string (input-stream "1 2 3 4 5 6 7 8 9")
  (loop repeat 10 collect (read-char input-stream)))

Кроме базовых функций ввода/вывода, вы можете использовать высокоуровневый функционал Лисповых считывателя и печатальщика. Мы рассмотрим их в следующих разделах.

Потоки закрываются с помощью функции close. Другие функции для потоков включают streamp, open-stream-p, listen, peek-char, clear-input, finish-output.

Лисповый печатальщик

Самая главная функция для вывода это write. Функции prin1, princ, print, pprint являются обёрткой для write. Необязательный аргумент потока в каждой из этих функции по умолчанию равен стандартному потоку вывода. Ещё один полезный набор функций это write-to-string, prin1-to-string и princ-to-string.

(setf z
  '("животные"
    ("млекопитающие"
     ("кошачие" ("лев") ("тигр") ("котенок"))
     ("медведи" ("полярный медведь") ("серый медведь"))
     ("грызуны" ("белка") ("кролик") ("бобёр")))
    ("птицы" ("канарейка") ("голубь"))
    ("рептилии" ("черепаха") ("змея"))))
(prin1 z) ;; эквивалентно (write z :escape t)
(princ z) ;; эквивалентно (write z :escape nil :readably nil)
(write z :escape nil :pretty t :right-margin 40)
(write-to-string z :escape nil :pretty nil)

Более сложная и гибкая функция вывода это format(format destination control-string argument...). Эта функция с помощью управляющей строки control-string определяет то, как необходимо вывести аргументы argument (если они были) и выводит в destination.

Если destination:тогда вывод:
tв стандартный поток
потокв указанный поток
nilбудет возвращён как строка

Управляющая строка представляет собой простой текст с управляющими директивами. Некоторые из них, частоиспользуемые, перечислены ниже.

~Wвывод как write; любой объект; obey every printer control variable
~Sвывод как prin1; любой объект; “стандартный” формат
~Aвывод как princ; любой объект; человекочитаемый формат
~D (или B, O, X)десятичный (или бинарный, восьмеричный, шестнадцатиричный) формат числа
~F (или E, G, $)фиксированный (экспоненциальный, общий, денежный) формат числа с плавающей точкой
{/control-string/}вывод списка; циклично использует управляющую строку control-string для форматирования элементов списка пока он не закончится
~%перевод строки
~&перевод строки, но только если текущая не пустая
~~вывод тильды
~*игнорирование текущего элемента
~/newline/игнорировать перевод строки и любый последующие пробелы (позволяет разбивать длинные управляющие строки на несколько)

Многие управляющие директивы принимают “аргументы” – дополнительные числа или специальные символы между ~ и самой последовательностью. Например, аргумент для многих директив указывает ширину столбца. Для подробностей смотрите документацию для каждой директивы. В месте “аргумента” для директивы, символ v обозначает следующий аргумент функции format, тогда как символ # обозначает число предыдущих аргументов функции format.

;; форматирование счёта
(loop for (code desc quant price) in
  '((42 "Дом" 1 110e3) (333 "Автомобиль" 2 15000.99) (7 "Конфета" 12 1/4))
  do (format t "~3,'0D ~10A ~3D @ $~10,2,,,'*F~%" code desc quant price))

(defun char-* (character number)
  "Возвращает строку длинной NUMBER заполненную символами CHARACTER."
  (format nil "~v,,,vA" number character ""))
;; но (make-string number :initial-element character) лучше

;; вывод счёта ещё раз в одну строку
(format t "~:{~3,'0D ~10A ~3D @ $~10,2,,,'*F~%~}"
 '((42 "Дом" 1 110e3) (333 "Автомобиль" 2 15000.99) (7 "Конфета" 12 1/4)))

;; список с запятыми-разделителями
(loop for i from 1 to 4 do
  (format t "~{~A~^, ~}~%" (subseq '(1 2 3 4) 0 i)))

;; опять список с запятыми разделителями, но умнее
;; (использует фичи, которые мы не рассматривали
(loop for i from 1 to 4 do
  (format t "~{~A~#[~; и ~:;, ~]~}~%" (subseq '(1 2 3 4) 0 i)))

(loop for i from 1 to 4 do
  (format t "~{~A~#[~;~:;,~]~@{~#[~; and ~A~:; ~A,~]~}~}~%"
      (subseq '(1 2 3 4) 0 i)))

;; опять вывод счёта, но умнее
;; с запятыми в ценах
(loop for (code desc quant price) in
  '((42 "Дом" 1 110e3) (333 "Автомобиль" 2 15000.99) (7 "Конфета" 12 1/4))
  do (format t "~3,'0d ~10a ~3d @ ~{$~7,'*:D~3,2F~}~%"
         code desc quant (multiple-value-list (floor price))))

Упражнение: Создайте (print-properties plist &optional stream) для вывода списка свойств в поток stream как показано ниже. Поток stream по-умолчанию должен быть равен *standard-output*.

(print-properties '(course CSE-202 semester "Summer 2004"
            room "Baldy 21" days "MR" time (10.30 11.30)))
-->
course=CSE-202
semester="Summer 2004"
room="Baldy 21"
days="MR"
time=(10.3 11.3)

Считыватель

Основной функцией ввода (чтения) является функция read. Кроме неё бывает удобна функция read-from-string.

(with-input-from-string (input-stream "(a b c)")
  (read input-stream))

(with-input-from-string (input-stream "5 (a b) 12.3 #\\c \"foo\" t")
  (loop repeat (read input-stream)
    do (describe (read input-stream))))

Ниже представлена функция чтения списка свойств в том формате, в котором мы сделали вывод в прошлом разделе.

(defun read-properties (&optional (input-stream *standard-input*))
  "Считывает список свойств из потока INPUT-STREAM.
Входящие данные должны содержать пару свойство-значение каждое в отдельной строке
в форме СВОЙСТВО=ЗНАЧЕНИЕ PROPERTY-NAME=VALUE.  СВОЙСТВО PROPERTY-NAME должно быть 
Лисповым символ.  ЗНАЧЕНИЕ VALUE может быть любым читабельным объектом."
  (loop for line = (read-line input-stream nil nil)
    while line
    for pos = (position #\= line)
    unless pos do (error "bad property list format ~s" line)
    collect (read-from-string line t nil :end pos)
    collect (read-from-string line t nil :start (1+ pos))))

(setf p1 '(course CSE-202 semester "Summer 2004"
       room "Baldy 21" days "MR" time (10.30 11.30)))
(setf p2 (with-output-to-string (stream)
       (print-properties p1 stream)))
(setf p3 (with-input-from-string (stream p2)
           (read-properties stream)))
(equal p1 p3)

На практике, мы можем захотеть больше проверок на ошибки, потому что read-properties прекрасно принимает такой ввод:

(with-input-from-string (stream "привет мир = 1 2 3")
  (read-properties stream))

Однако, этот весь пример немного выдуманный, тогда как если вы хотите сохранить список свойств или ассоциированный список в файле (например, конфигурационном файла для вашего приложения), вы можете просто написать готовый список в файл вместо форматирования его данных. Тогда вы и из файла можете просто прочесть список с конфигурацией.

Мы сможем сделать более осмысленное упражнение после того, как поговорим о Лисповых “объектах” – то есть, экземплярах классов. Тогда как экземпляры не имеют читабельного (для Лиспа) формата вывода, частая задача состоит в том, чтобы вывести экземпляры в читабельном формате, например, в виде списка, чтобы была возможность прочесть их обратно. Теперь следующее упражнение более осмысленное, чем пример со списком свойств.

Управжнение: Мы решили использовать компактный формат файла для больших, разряжённых массивов. Формат такой: dimensions default-value index1 value1 index2 value2 .... Например:

(100 100) 0
(30 30) 30
(60 60) 60

Напишите функцию (read-sparse-array &optional input-stream) для чтения данного формата и создания массива.

Небольшой проект: Напишите форматировщик оглавления. Предположим, что ввод это последовательность строк, каждая строка начинается с n-ного количество пробелов (n ≥ 0), n обозначает уровень данного заголовка. Например, вот оглавление для данного руководства для ввода/вывода:

Input/output
 Streams
  File streams
  String streams
 Stream input and output functions
 Other stream functions
The printer
 Print functions
 Format
  Destinations
  Control directives
  Examples
The reader
Ввод/вывод
 Потоки
  Файловые потоки
  Строковые потоки
 Функции для ввода/вывода в/из потока
 Прочие функции для потоков
Лисповый печатальщик
 Функции вывода
 Format
  Направления
  Управляющие директивы
  Примеры
Считыватель

Прочтите оглавление из потока ввода, пронумеруйте его, правильно расставьте отступы и напечатайте в поток вывода. Ниже представлен один из возможных форматов.

  I. Ввод/вывод
      A. Потоки
          1. Файловые потоки
          2. Строковые потоки
      B. Функции для ввода/вывода в/из потока.
      C. Прочие функции для потоков
 II. Лисповый печатальщик
      A. Функции вывода
      B. Format
          1. Результат
          2. Управляющие директивы
          3. Примеры
III. Считыватель

Ваш форматтер для оглавления должен использовать список (F/0 /F/1 …). Каждый элемент /Fn представляет собой список вида (width labeler), где width это ширина отметки для названия уровня n и labeler это функция, которая принимает число, и возвращает строку для отметки уровня n. Например, оглавление выше было отформатированно с помощью следующего списка:

(defparameter *outline-format-1*
    (list
     (list 6 #'(lambda (n) (format nil "~@R." n)))
     ...

Метки нулевого уровня имеют ширину в шесть символов, и функция для отметок возвращает римскую цифру. Автор предлагает вам самим додумать, каким должен быть весь список для форматтера.

Сначала, напишите функцию (read-outline &optional input-stream), которая читает план с отступами и создаёт список со всеми строками и их уровнями.

((0 "Ввод/вывод")
 (1 "Потоки")
 (2 "Файловые потоки")
 (2 "Строковые потоки")
 (1 "Функции для ввода/вывода в/из потока")
 (1 "Прочие функции для потоков")
 (0 "Лисповый печатальщик")
 (1 "Функции вывода")
 (1 "Format")
 (2 "Результат")
 (2 "Управляющие директивы")
 (2 "Примеры")
 (0 "Считыватель"))

Затем напишите функцию (print-outline outline outline-format &optional output-stream) для форматирования данного списка в соответствие с форматом outline-format.

Объектная система Коммон Лиспа (CLOS)

Введение
Объектная система Коммон Лиспа (*C*ommon *L*isp *O*bject *S*ystem - далее CLOS) позволяет создавать классы (с множественным наследованием) и обобщённые (полиморфные) функции.

Авторы дадут только упрощённое введение в CLOS. Много деталей останется за кадром.

Многие (но не все) стандартные Коммон Лисповые типы также являются классами. Вот они: classes.gif (Найдите два класса с несколькими родителями.)

Обобщённые функции
Обобщённая функция это набор методов с одинаковыми именами и “совместимыми” лямбда-списками, при этом обязательные параметры могут указывать на класс для их аргументов.

Пример 1: Давайте создадим обобщённую функцию, которая будет выводит классы для заданных объектов.

(defmethod id ((x number))
    "Выводит сообщение о том, что это число."
    "Я число.")

(defmethod id ((x sequence))
    "Выводит сообщение о том, что это последовательность."
    "Я последовательность.")
    

Протестируйте id для нескольких чисел и последовательностей с разными подтипами.

Протестируйте id для нескольких объектов, не чисел и не последовательностей.

Применяемый метод выбирается для самого нижнего возможного класса. Упражнение: добавьте метод id для некоторых подклассов числа (number) или последовательности (sequence), и протестируйте, что они используются в подходящих случаях.

Когда класс C имеет два родительских класса, и существует метод для каждого из родителей, какой же из них будет использован? Это определяется с помощью списка предшествующих классов для C.

Пример 2: Создадим отношение < между числами и символами, таким образом списки содержащие числа и символы будут отсортированы лексикографически. Числа должны сортироваться с помощью cl:<, символы с помощью string<, и любое число должно быть < чем любой символ. Решение:

(defpackage :closExercises
  (:shadow cl:<))

(in-package :closExercises)

(defmethod < ((n1 number) (n2 number))
  "Если число n1 меньше чем n2 возвращает t, иначе nil."
  (cl:< n1 n2))

(defmethod < ((s1 symbol) (s2 symbol))
  "Если символ s1 меньше чем s2 возвращает t, иначе nil."
  (string< s1 s2))

(defmethod < ((n number) (s symbol))
  "Возвращает t, так как числа меньше символов."
  t)

(defmethod < ((s symbol) (n number))
  "Возвращает nil, так как символы не меньше чисел."
  nil)

(defmethod < ((list1 list) (list2 list))
  "Если список list1 меньше чем  list2 возвращает t, иначе nil."
  ;; Списки упорядочиваются лексикографически в соответствие с их элементами.
  (cond
   ((endp list1) list2)
   ((endp list2) nil)
   ((< (first list1) (first list2)) t)
   ((< (first list2) (first list1)) nil)
   (t (< (rest list1) (rest list2)))))
    

Упражнение: Проверьте методы.

Обобщённые функции могут использоваться также как и обычные. Например, мы может определить > следующим образом:

;;; Сначала скрываем cl:>.
(shadow 'cl:>)

;;; Затем создаём >.
(defun > (x y)
  "Если x больше y возвращает t, иначе nil."
  (< y x))
    

Заметьте, что > автоматически работает для тех же классов, для которых работает <.

Теперь давайте сделаем < с помощью defgeneric и добавим строки и списки. Списки должны ставиться после символов, списки должны быть после строк. То есть, любое число < любого символа, любой символ < любой строки, и любая строка < любого списка, числа должны сравниваться с помощью cl:<, символы и строки – с помощью string< и списки так, как показано ниже. (Нам действительно нужно писуть 16 различных методов?) Решение:

(defpackage :closExercises
  (:shadow cl:< cl:>))

(in-package :closExercises)

(defgeneric < (obj1 obj2)
  (:documentation "Если объект obj1 меньше чем объект obj2 возвращает t, иначе nil.")

  (:method ((n1 number) (n2 number))
       "Если число n1 меньше чем число n2 возвращает t, иначе nil. Использует cl:<."
       (cl:< n1 n2))

  (:method ((s1 symbol) (s2 symbol))
       "Если символ s1 меньше чем символ s2 возвращает t, иначе nil. Использует string<."
       (string< s1 s2))

  (:method ((s1 string) (s2 string))
       "Если строка s1 меньше чем строка s2 возвращает t, иначе nil. Использует string<."
       (string< s1 s2))

  (:method ((list1 list) (list2 list))
       "Если список list1 лексикографически меньше чем список list2 возвращает t, иначе nil."
       ;; Списки упорядочиваются лексикографически в соответствие с их элементами
       (cond
        ((endp list1) list2)
        ((endp list2) nil)
        ((< (first list1) (first list2)) t)
        ((< (first list2) (first list1)) nil)
        (t (< (rest list1) (rest list2)))))

  (:method ((obj1 t) (obj2 t))
       "Если объект obj1 меньше чем объект obj2 возвращает t, учитывает сравнение разных типов."
       (check-type obj1 (or number symbol string list))
       (check-type obj2 (or number symbol string list))
       (member obj2
           (member obj1 '(number symbol string list) :test #'typep)
           :test #'typep)))

(defun > (x y)
  "Если x больше чем y возвращает t, иначе nil."
  (< y x))
    

Новая форма: check-type.

Упражнения:

  1. Протестируйте то, что написали.
  2. Добавьте строковые символы, которые ставятся между числами и символами и сравниваются с помощью <.
Классы
Объекты (экземпляры класса) создаются с помощью (make-instance class ...).

CLOS классы создаются с помощью defclass.

Класс может иметь три специальные опции, мы будем использовать только одну :documentation.

Класс также может содержать набор слотов, каждый из которых имеет свойства, которые были заданы в параметрах слота. Вот эти параметры:

  • :documentation Строка документации.
  • :allocation Значение :instance означает, что этот слот локальный для каждого экземпляра, значение :class означает, что слот один для всех экземпляров класса.
  • :initarg Символ, который потом используется в make-instance для задания значения для слота.
  • :initform Форма, которая вычисляется при создании экземпляра, и возвращает значения для слота.
  • :reader Символ, которые задаёт имя метода, который возвращает значение слота для заданного экземпляра.
  • :writer Символ, который задаёт имя для метода, который используется для установки значения в слот экземпляра. Если setSlot является символом, то итоговая форма выглядит так (setSlot value instance)
  • :accessor Символ, которые задаёт имя для метода, который используется и для чтения и для установки значения в слот экземпляра.
  • :type Тип данных разрешённых в слоте.

    Даже если ни :write, ни :accessor не были указаны значение слота можно получить или изменить с помощью slot-value. Например:

    (setf (slot-value object slot-name) value)
            

Можно использовать

(defmethod initialize-instance :after ((object class) &rest args)
    ...)
    

это позволит инициализировать слоты после того как были заданы :initarg и :initform.

В качестве примера, мы создадим классы для взвешиваемых твёрдых веществ и класс для весов. Они определены в файле solids.cl.

Упражнения:

  1. Скопируйте solids.cl в свой файл и протестируйте его.
  2. Добавьте слот в класс весов,
  3. Добавьте метод (removeObject scale object) для убирания объекта с весов. Все слоты должны быть правильно настроены, а removeObject должен сигнализировать ошибку, если объект для убирания не находится на весах.

© 2004 Стюарт Шапиро, Дэвид Пирс. Все права защищены.

Стюарт Шапиро <shapiro at cse.buffalo.edu>

Дэвид Пирс <drpierce at cse.buffalo.edu>

About

Translation of Uber Lisp article into russian.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published