Введение в Common Lisp для профессионалов Delphi/SQL

 

Цель данного документа – дать направления поиска информации и указать эффективные подходы к работе для профессиональных разработчиков, начинающих работу с Common Lisp. Рассмотрена версия Lispworks, кое-где нестандартные возможности Lispworks выделены зелёным. Мои расширения выделены синим цветом. Красным цветом выделены понятия, которые обязательно знать наизусть. Данный документ написан в меру знаний и отражает личные предпочтения автора.

 


Содержание

Справочные материалы и книги

Обозначения

Отличительные особенности лиспа (по сравнению с Дельфи)

Сборка мусора

Динамическая разработка

Читатель/писатель

Макросы

Любимые засады новичков

Отладчик очень близко

Недопечатанные формы

Стираем подсказку и застреваем в строке ввода команды

Изучение исходного текста в IDE

Списки. Квазицитирование

Вид списка на печати и в памяти

Работа со списками в редакторе

Как создать список. Квазицитирование

Переменные и присваивание

Оператор присваивания setf, понятие места

Связывание и присваивание, setf, понятие места

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

Глобальные (специальные) переменные

Замыкания

Переменные и типы

Управляющие конструкции

Определение и вызов функции, возврат значений, if, циклы, локальные функции, указатели на функции, try..except

try..finally

Overload

Организация проекта. Файлы, пакеты, eval-when, #., asdf-системы, динамическая разработка

Определения понятий

Таблица аналогий

Динамическая разработка, основные сценарии

Встроенные типы данных

Символы

Списки

Массивы и строки

Хеш-таблицы

Числа

Полиморфные (родовые) функции, структуры и классы

Родовые функции

Структуры

Классы

eval и макросы

Eval

Макросы

Отладка

Три отладчика: консольный, жук и степпер

Для отладки поместите код в файл

Изучение состояния программы в отладчике

Листенер во время отладки

Разновидности ошибок и их локализация

Трассировка и отладка print-ами

Остановы и пошаговое исполнение

Assert, with-the1

Unit-тестирование

Профайлер

Трассировка SQL

Контрольные вопросы и практические задания


Справочные материалы и книги

Язык Common Lisp Второе издание 

Стандарт Common Lisp, в удобочитаемой форме, около половины страниц переведена на Русский

 

Common Lisp Hyperspecстандарт Common Lisp

Обычно статьи из Hyperspec доступны в IDE по нажатию горячей клавиши на имени функции

 

Книга «Practical Common Lisp» в Русском переводе

Хорошее практическое введение (более 400 страниц)

 

The Common Lisp Cookbook

Сборник коротких рецептов

Обозначения

Красным цветом обозначены понятия, смысл которых нужно знать наизусть.

Отличительные особенности лиспа (по сравнению с Дельфи)

Сборка мусора

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

Динамическая разработка

Динамическая разработка – ключевое преимущество лиспа, которое очень помогает в разработке. Как правило, Лисп позволяет менять программу без её перезапуска, в этом он подобен SQL.

defclass работает аналогично CREATE TABLE/ALTER TABLE. Он позволяет добавить поля в класс или удалить их, с обновлением уже существующих экземпляров.

defstruct переопределяет структуру, но при этом уже существующие экземпляры становятся устарвешими (obsolete) и попытка обращения к ним вызывает ошибку.

defun, defgeneric и defmethod работают аналогично CREATE OR ALTER PROCEDURE, причём можно менять набор параметров. Если функция в данный момент выполняется, будет продолжать выполняться старое тело. Независимо от этого, все новые входы в данную функцию (в т.ч. рекурсивно из уже выполняющегося старого тела) будут использовать новое тело. В отладчике в некоторых случаях можно перезапустить уже выполняющуюся функцию, при этом будет вызвано новое тело с теми же параметрами.

Также можно динамически создавать/менять пространства имён (пакеты) defpackage, уничтожать их (delete-package), включать и исключать идентификатор (символ) в/из пространства имён.

Возможности динамического изменения программы могут быть ограничены при сборке с оптимизациями.

Также в Лиспе есть listener, который по своим возможностям аналогичен интерактивному SQL. listener позволяет немедленно выполнять вычисления, не проходя цикл написания, сборки и запуска приложения. В отладчике, listener позволяет работать со значениями локальных переменных на разных уровнях стека. Это очень удобно для разработки, тестирования и отладке программы – вместо запуска всей программы удобно запускать её отдельные функции.

Читатель/писатель

Читатель (начинка функции read) – это парсер. Другие примеры парсеров – парсеры XML, JSON, dfm. Также существует парсер Object Pascal в Delphi, который строит из текста дерево разбора. В Delphi это дерево недоступно для пользователя, оно используется только внутри компилятора для генерации машинного кода. В Лиспе дерево, получающееся в результате работы парсера, доступно пользователю. В этом смысле читатель Лиспа больше похож на парсер dfm, XML или JSON, который разбирает данные, а не код. Особенности работы читателя:

-         форматом ввода являются S-выражения (скобки, точки, атомы и др.). Скобки и точки отвечают за группировку данных в деревья. Атомы являются листьями деревьев.

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

-         также можно задать формат для ввода объектов произвольных типов.

Читатель может использоваться как для ввода исходного текста программы, так и для ввода произвольных данных из текстового формата. Разница – не в структуре этих данных, а в том, что с ними в дальнейшем делается. Поэтому говорят, что в Лиспе код=данные, что также обозначается словом homoiconicity.

 

Писатель – это начинка функции print. Print печатает в текстовый поток любой объект, причём можно настраивать печать своих и некоторых встроенных типов. Можно считать функцию print расширяемым аналогом функции write из Дельфи. Строки, числа, символы, списки, массивы, структуры, хеш-таблицы имеют формат вывода по умолчанию, при котором выводится содержимое этих объектов. Для классов метод печати по умолчанию не печатает содержимое объекта.

Читатель и писатель согласованы. Во многих случаях, если писатель что-то написал, то читатель может прочитать написанное и построить "такой же" объект, либо вернуть ссылку на тот же самый объект, который ранее был напечатан. Те объекты, которые нельзя прочитать, по соглашению должны печататься так: #<какой-то текст>. При попытке читателя прочитать это представление возникает ошибка. Также писатель может отказаться печатать слишком длинные или глубоко вложенные списки (это настраивается), тогда часть списка будет заменена многоточием и при попытке его чтения также возникнет ошибка.

 

С помощью print/read возможен ввод-вывод данных с циклическими ссылками.

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

Макросы

Макросы делают компилятор расширяемым и позволяют создавать новые конструкции языка.

Могущество макросов основано на порядке сборке программы на лиспе. Программа собирается в порядке: запустили программу, скомпилировали внутри этой программы модуль, прилинковали его (к уже работающей программе), скомпилировали следующий и т.п.

Макросы - механизм, позволяющий строить код во время компиляции, используя для преобразования кода вызовы ранее определённых функций. Например, на макросах можно сделать препроцессор SQL, который проверяет правильность встроенного в программу запроса во время компиляции программы.

Макросы часто представляют из себя шаблоны, в которых большая часть постоянна и лишь некоторая, специально выделенная часть подставляется. Такие шаблоны похожи на списки, а операция подстановки называется квазицитирование.

Любимые засады новичков

При работе в listener легко сбиться с толку. Чтобы это случалось не слишком часто, нужно понимать следующие вещи:

Отладчик очень близко

Обычно подсказка listener выглядит как CL-USER NN > , где NN – возрастающий порядковый номер, который увеличивается на 1 после каждого ввода. Попробуем ввести в listener выражение (/ 1 0). Мы увидим следующее:

Error: Division-by-zero caused by / of (1 0).

...

CL-USER NN : 1 >

Суффикс : 1 говорит, что мы находимся в отладчике. Об этом же говорит то, что на панели инструментов стала доступна кнопка с изображением жука (бага). Нажав на неё, мы попадём в графический отладчик, в нём увидим стек и т.д. Данное окно графического отладчика будет связано с окном listener, хотя это не видно.

Подсказка отладчика мало чем отличается от обычной подсказки листенера не только по виду, но и по возможностям: в ней также можно проводить произвольные вычисления, как и в обычной подсказке. Это очень хорошо (в Delphi так нельзя), но это может сильно сбивать с толку.

Попробуем теперь снова ввести (/ 1 0) в окне listener. Подсказка изменится:

CL-USER NN : 2 >

двойка говорит о том, что мы находимся в отладчике рекурсивно, и уровень вложенности отладчиков равен двум. В окне графического отладчика можно видеть, что функции / и error присутствуют в стеке дважды.

Теперь можно (в окне listener) набрать :a и <Enter> (или Ctrl-D) - это примерно то же, что бросить исключение EAbort. Выпадем обратно в первый уровень отладчика. Набрав :a <Enter> ещё раз, вернёмся в нормальное состояние интерпретатора. Во время :a защитный код (аналог finally) выполняется, как и при обычном выполнении программы. Поэтому, если мы "упали" в отладчик в каком-то месте, выход из отладчика по :a является безопасным (хотя, конечно, всё зависит от конкретной ошибки и ситуации).

Данный отладчик, хоть и не даёт возможности пошагового исполнения (эта возможность предоставляется отдельным средством – степпером), обладает другими очень полезными возможностями, во многом мы терпим лисп ради них. Помимо возможности вычисления произвольных выражений в контексте программы, отладчик обычно предлагает один или несколько "перезапусков" (restarts), например, попробовать вычисление ещё раз, вернуть другое значение вместо вызвавшего ошибку и т.п. Рестарты нужно читать и использовать.

Для более серьёзной отладки используется графический отладчик (жук), который можно вызвать из консольного с помощью иконки н панели управления. О нём см. ниже.

Недопечатанные формы

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

Строковый литерал (строка в кавычках) также может быть многострочной. Как понять, находится ли листенер в состоянии чтения команды или вычисления? После запуска команды на выполнение её текст окрашивается в красный цвет. Если есть сомнения, всегда можно нажать Ctrl-Break (или на вот такой кирпич ) и посмотреть на стек. Также есть диспетчер процессов, works/tools/process browser, но не забудьте после открытия нажать F5 для обновления состояния.

Другие сведения о локализации ошибок см. в разделе об отладке.

Стираем подсказку, застреваем в строке ввода команды

Можно спокойно стереть подсказку ">" листенера и тогда будет вообще непонятно, завис ли Listener и что с ним случилось. Вы будете писать какие-то команды, нажимать Enter и ничего не будет происходить. В случае любых недоразумений жмите Ctrl-Break и затем :a. Это, как правило, помогает.

При определённых условиях (вводе разного рода команд) курсор может "застрять" в строчке ввода команды, располагающейся внизу экрана (она называетя minibuffer). Если Вы не знаете, зачем туда попали, жмите ESC, пока не отпустит, а затем нажмите стрелку вниз, чтобы сбросить набранные в отчаянии лишние нажатия ESC.

Изучение исходного текста в IDE

В загруженной программе, Alt-. показывает определение переменной, ф-ии, класса, родовой ф-ии, макроса и многих других объектов. Works/Tools/System browser показывает системы (в т.ч. asdf). Works/Tools/Symbol browser позволяет искать символы по части названия (то же делает и apropos).

Списки. Квазицитирование

Вид списка на печати и в памяти

Список выглядит в тексте так:

( элемент1 элемент2 ... элементN )

Внутри список (1 2 3 4) представляет из себя кособокое дерево объектов типа cons: (картинка отсюда http://eli.thegreenplace.net/2007/08/10/sicp-section-221/

Т.е, наиболее быстрые операции над списком – это операции над его первым элементом, а доступ к произвольному элементу имеет сложность O(length(x)) Разные списки могут иметь общий хвост.

Работа со списками в редакторе

Исходный текст по большей части представляет из себя печатную форму списков (и преобразуется в списки на этапе чтения программы из файла исходника), поэтому очень важно уметь работать с ними в редакторе. Над списками доступны команды:

-         переход от закрывающей к открывающей скобке и обратно

-         удаление списка до или после курсора в буфер обмена (kill)

-         переход на следующий/предыдущий список и на список верхнего уровня

-         перестановка двух списков перед курсором

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

Также можно отметить, что редактор подсвечивает парную скобку и раскрашивает скобки в разные цвета.

Поскольку синтаксис лиспа неудобный, при работе важную роль играют отступы. IDE автоматически выставляет отступы, для этого после нажатия Enter при начале новой строки нужно также нажимать Tab. Также есть команда Alt-. indent form, которая выравнивает целое определение (нужно стоять в начале определения).

Как создать список. Квазицитирование

Чтобы создать новый список в listener-е, можно написать:

> '(a "b" a 123)

(a "b" a 123) ; это напечатает листенер

Здесь мы построили список из четырёх элементов. Первый и третий элементы идентичны – это символ с именем "A". Второй элемент – строка "b", четвёртый – число 123. Лисп печатает результат нашей работы. Мы использовали одиночную кавычку - "цитирование", чтобы листенер не воспринял нашу команду как вызов функции a. Через некоторое время наш список может стать мусором, т.к. мы его ничему не присвоили, а символ с именем "А" останется, т.к. при чтении он автоматически попал в текущее пространство имён.

Двухуровневый список можно построить так:

> '(a (a))

Он будет содержать две ссылки на тот же самый символ с именем "A".

Здесь второй элемент списка – список из одного элемента (символа с именем "A").

Другой способ построения такого же списка:

> (list 'a "b" 'a (+ 120 3))

Здесь для построения используется функция list. Эта функция вычисляет все свои аргументы и составляет из них свежий список. Последний аргумент – это вычисление выражения 120+3=123.

Третий способ:

> `(a "b" a ,(+ 120 3))

Здесь применено квазицитирование. Апостроф означает, что следующее за ним лисп-выражение – это шаблон. Данные в шаблоне не вычисляются. Но если в шаблоне встречается запятая или ",@" , то следующее выражение – подставляемое, оно вычисляется. Данная конструкция чем-то аналогична функциям форматного вывода и её можно смоделировать в Дельфи так:

format('(a,''b'',a,%d)',[120+3])

Отличие состоит в том, что в Дельфи эта конструкция работает со строками, а в Лиспе – с деревьями.

Шаблоны и обычные цитаты могут быть вложенными и могут быть вложены друг в друга.

Переменные и присваивание

Оператор присваивания setf, понятие места

setf – это присваивание, оно вполне аналогично := в Delphi.

Связывание и присваивание, setf, понятие места

Связывание - это одновременное объявление и инициализация переменной.

Присваивание - то же самое, что := в Delphi. То, чему можно присваивать, называется место (place). В частности, местами являются имена переменных, элементы списков и массивов, подстроки строк, значения хеш-таблиц, соответствующие определённому ключу и т.п. Можно определить новые виды мест. Общий вид присваивания:

(setf место1 значение1 ... местоN значениеN)

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

Локальные переменные полностью аналогичны локальным переменным Delphi, но есть следующие отличия:

В Дельфи переменные определяются до слова begin с помощью слова var, в Лиспе они определяются внутри тела функции с помощью разнообразных конструкций (let, destructuring-bind, multiple-value-bind (mlvl-bind), with-open-file и т.п.) и их область действия – до закрывающей скобки этой конструкции. Возможно несколько одноимённых переменных внутри одной функции на разных уровнях вложенности - внутренние затеняют внешние.

Глобальные (специальные) переменные

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

Основная особенность работы с глобальными переменными: когда такой переменной делается let, локальная переменная с таким именем НЕ создаётся. Вместо этого, происходит связывание глобальной переменной. Именно, старое значение глобальной переменной запоминается и временно заменяется на новое. Теперь, во всех функциях, будет видно значение, заданное let. Последующие setf могут его менять. После выхода из области действия let, ранее запомненное в момент выполнения let значение восстанавливается. Пример работы в однопоточном приложении:

 

(defparameter *glob* 1)

 

 

 

(progn ; составной оператор, то же, что begin..end;

   (let ((*glob* 2))

 

      (format t "связана в ~S" *glob*)

      (setf *glob* 3)

      (format t "после setf ~S" *glob*)

 

 

   )

   (format t "после выхода из let ~S" *glob*)

)

 

var glob:integer=1;

 

 

var saveGlob:integer;

begin

try

  saveGlob:=glob; glob:=2;

  writeln(format('связана в %d',[glob]));

  glob:=3;

  writeln(format('после := %d',[glob]));

finally

  glob:=saveGlob;

  end;

writeln(format('после выхода из let',[glob]);

end.

 

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

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

1. переменная создаётся с помощью defparameter (или с помощью defvar, если переменная должна быть инициализирована только один раз за жизнь программы)

2. Имя переменной должно быть окружено звёздочками, *my-variable*.

3. Приложение должно собираться без предупреждений "variable ... assumed special". В частности, глобальная переменная должна быть объявлена по течению процесса сборки до любого её использования.

Многонитевые приложения

В многонитевом (многопоточном, multi-thread) приложении принципиально возможно два режима работы с глобальной переменной. У переменной есть общее для всех нитей глобальное значение. Оно действует в тех нитях, где переменная не связана с помощью let. Присваивание с помощью setf в одной такой нити меняет её значение во всех остальных, т.е., она ведёт себя как var в Delphi. Такая переменная является потокобезопасной и не вернёт мусор, если её значение меняется в другой нити в момент чтения.

 Когда переменная в данной нити связана с помощью let, общее между нитями глобальное значение не меняется. Вместо этого она на время действия let получает в данной нити отдельное глобальное значение, видное только этой нити (как threadvar в Delphi). При вложенных связываниях в этой же нити, или при присваиваниях во время действия let меняется только отдельное глобальное значение, видимой в этой нити, остальные нити не затрагиваются этим изменением.

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

Можно установить перечень переменных, которые будут связаны для каждой новой нити при её создании, см. mp:*process-initial-bindings*, и их начальных значений.

Узнать, связана ли переменная, можно в отладчике (жуке), включив меню view/bindings. Либо можно искать имя переменной во всех исходных текстах и смотреть, что с ней происходит. При этом нужно помнить, что let является не единственной конструкцией связывания.

Замыкания

Генератор анонимных функций lambda имеет два аспекта.

Во-первых, при каждом выполнении он возвращает новую функцию без имени.

Во-вторых, эта функция может ссылаться на локальные переменные, существующие там, где вызван оператор lambda, при этом, время жизни локальных переменных продлевается на весь период жизни функции, созданной lambda.

Замыкания реализованы в Delphi 2009, хотя автору неизвестно, насколько полноценна их реализация.

type
  TProc = reference to function(x: Integer): function;
 
function Foo(a: Integer): TProc;
begin
  Foo := function(b: Integer): Integer;
  begin
    Result := a + b;
  end;     

end;

 

 

 

(defun Foo (a)

 

    (lambda (b)

     

        (+ a b)

        )

)

Переменные и типы

Переменные в лиспе обычно не типизированы (имеют тип t, что означает "любой объект"), хотя можно сделать их типизированными с помощью declare или with-the1. В этом случае тип значения будет проверяться на соответствие, если только проверка не отключена в компиляторе. Автоматическое преобразование типов в лиспе не применяется. Узнать тип объекта можно с помощью (type-of объект). Узнать тип переменной стандарт не позволяет, автор пользуется budden-tools::variable-type-or-class (см. применения в библиотеке). Также есть hcl:variable-information, но она почему-то не показывает тип.

Работа с нетипизированными переменными в лиспе может показаться похожей на работу с типом variant в Delphi, но это не так. При работе с типом variant происходят автоматические преобразования типов (например, можно превратить числовой вариант в строку и наоборот), а лисп этого не делает. Преобразования типов в лиспе всегда являются явными (например, coerce, prin1-to-string, read-from-string, parse-integer, string)

 

Управляющие конструкции

Определение и вызов функции, возврат значений, if, циклы, локальные функции, указатели на функции, try..except

Delphi

CL

function f(s:string;o:TObject):string;

begin;

s:=s+s;

result:=s+o.ClassName;

end;

(defun f (s o)

 

  (setf s (str+ 'string s s))

  (str++ 'string local-s (type-of o))

  )

; составные объекты (массивы, структуры, экземпляры

; классов) передаются в функции так же, как объекты

; в Delphi. При модифицирующих операциях со строками

; меняется исходная строка

 

 

 

 

 

procedure p(var o:TObject);

begin

o:=someOtherObject;

end;

 

p(x);

для возврата нескольких значений из функции можно использовать values и multiple-value-bind. Для произвольных действий с переменной можно использовать макрос. Имитация var параметра в budden-tools:

 

(defun p (o)

  (with-byref-params (o)

    (setf o someOtherObject)

    )

 

(p (byref x))

 

If j=0 then result:=0 else result:=j+4;

(if (= j 0) 0 (+ 4 j))  ; if возвращает значение

if a=1 then

 

  writeln('один')

else if a=2 then

  writeln('два')

else

  writeln('много');

 

(cond

  ((= a 1)

    (print "один"))

  ((= a 2)

    (print "два"))

  (t

   (print "много")

   ) ; тоже возвращает значение, в отличие от Delphi

for i:=0 to N-1 do

  begin

  if i=5 then continue;

  if i=7 then break;

  writeln(i);

  end

(dotimes (i N) ; только от 0 до N-1!

   (when (= i 5) (go :continue))

   (when (= i 7) (return nil))

   (print i)

   :continue)

ЛИБО

(iter ; мощнее, но хуже поддерживается отладчиком

  (:for i :from 0 :to (- N 1))

  (:when (= i 5) (:next-iteration))

  (:when (= i 7) (return nil))

  )

while i>j do

 

  inc(i,-1);

(loop

  (unless (> i j)

     (return nil))

  (incf i –1)

  )

ИЛИ

(iter ; мощнее, но хуже поддерживается отладчиком

  (:while (> i j))

  (incf i –1)

  )

procedure outer; // пример локальной функции

  var v:variant;

  procedure inner;

  begin

  v:=v+1;

  end

begin{procedure outer}

v:=0;

inner;

end;

(defun outer ()

  (let ((v 0))

    (flet ((inner ()

 

              (incf v)

               ))

 

       (inner)

       )))

ЛИБО

(defun outer ()

  (proga ; нестандартно, эквивалентно предыдущему,

         ; но меньше скобок и хуже поддерживается отладч.

     (let v 0)

     (flet inner ()

        (incf v))

     (inner)

     ))

    

try

 

  op1;

  op2;

except

  on E1:class1 do

    begin

    writeln(E1.message);

    op4;

    end

  on E2:class2 do

    begin

    raise;

    end

  end;

(handler-case

  (progn

    op1

    op2)

 

  (class1 (e1)

 

    (princ e1)

    op4

    )

  (class2 (e2)

 

    (error e2)

    )

  ) ; ну вот, заодно и сам узнал  

 

// указатели на функции

var myFunc:

  function(x,y:integer):integer

  =@namedFunction

result:=myFunc(1,2);

 

(let ((myfunc #'namedFunction))

 

 

(funcall 'myfunc 1 2))

try..finally

Обычно, если нужно совершить какую-то очистку действия при выходе (даже аварийном) из функции, в Дельфи используется try..finally.

В Лиспе для этого используется аналогичная конструкция, unwind-protect, но, благодаря наличию макросов, можно сделать конструкцию очень лаконичной. Например, если определить

 

(defmacro with-guarded-x ((var &rest args) &body body)

  `(let ((,var (create-x ,@args)))

     (unwind-protect

       (progn ,@body)

       (clear-x ,var)

       )))

то вызов

 

(with-guarded-x (a arg1 arg2)

(do-somehing-1)

(do-something-2))

 

при компиляции развернётся в

 

 

 

(let ((a (create-x arg1 arg2)))

  (unwind-protect

     (progn

       (do-somehing-1)

       (do-something-2)

       )

    (clear-x a)

   ))

var a:x;

begin

a:=x.create(arg1,arg2);

try

 

  doSomething1;

  doSomething2;

finally

  freeAndNil(a);

  end;

Если нить исполнения (поток, процесс) убивается с помощью mp:process-kill, то ресурсы не будут освобождены до тех пор, пока объект не попадёт под сборку мусора, а это может занять неопределённое время. Для освобождения ресурсов в этой ситуации нужно использовать mp:ensure-process-cleanup

Overload

Вместо overload есть другие, более мощные инструменты:

1. Родовые функции. В отличие от Дельфовых overload, лисповые родовые функции смотрят не на тип формального параметра, а на тип фактического параметра, т.е.выбор того или иного варианта overload происходит не во время компиляции, а во время выполнения. Более подробно описаны в главе про ООП.

2. Необязательные параметры &optional, ключи &key, получение всех параметров в виде списка &rest. Синтаксис вызова функции лиспа примерно соответствует синтаксису команды операционной системы. Параметры в конце могут быть не заданы и они заменяются умолчаниями. Также могут быть заданы параметры ключи, например:

 

(position #\b "Abc" :test 'char-equal)

- использован параметр-ключ :test со значением 'char-equal.

Организация проекта. Файлы, пакеты, eval-when, #., asdf-системы, динамическая разработка

Структура проекта и процесс загрузки приложения на Лиспе гораздо сложнее, чем на Дельфи, поскольку:

- в Дельфи мало что можно поменять во время выполнения, а в Лиспе можно поменять почти всё

- понятие модуля в Дельфи являются монолитными, а в Лиспе оно разделено на несколько понятий

- многое из того, что  в Дельфи делается автоматически, в Лиспе нужно делать руками.

Определения понятий

Файл *.lisp – аналог *.pas

Файл *.ofasl – аналог *.dcu. Могут размещаться в разных экзотических местах, см. эти места при компиляции или ищите поиском по диску.

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

eval-when – аналог {$if файлКомпилируется} ... {$elseif файлЗагружается} ... {$endif}, позволяет выполнять тот или иной код на этапе компиляции и/или загрузки уже скомпилированного файла.

#. – позволяет выполнять код в момент чтения текста программы из файла (до компиляции).

asdf-система – аналог файла проекта или makefile. Типичная программа содержит множество взаимосвязанных систем asdf, каждая из которых имеет свой каталог и определение в файле *.asd.

Динамическая разработка – возможность менять код программы без её остановки.

 

Таблица аналогий

Delphi

CL

 

*.dpr

Скрипт загрузки, с помощью которого «голая» лисп-система превращается в программу (мы ещё не рассматриваем save-image, но это в другой раз). Содержит инструкции по компиляции и загрузке всей системы.

unit

1. unit как единица сборки – файл, включённого в asdf-систему или просто загружаемого из скрипта загрузки.

2. unit как пространство имён – пакет

interface, implementation

interface - внешние символы пакета (обычно – в файле каталогСистемы/package.lisp, иногда – в файле исходного текста, форма defpackage, def-merge-packages::!). Перечислены только имена сущностей, но не их типы.

Сами сущности (функции, макросы, типы, переменные) определены в исходном тексте без разбивки на interface и implementation

uses

Если рассматривать unit как единицу сборки, то зависимости указываются в asdf – системе. При неправильном порядке сборки, как правило, будут ошибки или предупреждения, а в худшем случае – молчаливая неправильная работа.

Если рассматривать unit как набор допустимых имён, то зависимости указываются в форме определения пакета (:use, :import-from). Но можно и без этого вызывать функцию из другого пространства имён с помощью префикса, главное, что символ должен существовать на момент чтения файла

initialization

Любые формы, записанные в исходном тексте файла .lisp, выполняются во время загрузки (за исключением eval-when).

Даже определение функции является не просто декларацией, а командой лисп-системе «в момент загрузки создай такую-то функцию».

Нужно учитывать, что в Дельфи сначала компилируется вся программа, а только потом запускается initialization первого модуля. В Лиспе, как правило, порядок действий другой: компиляция f1.lisp (если он требует перекомпиляции) – загрузка f1.ofasl, компиляция f2.lisp, загрузка f2.ofasl и т.п. При этом, для компиляции f2.lisp могут использоваться символы, функции и данные, определённые в f1.lisp. Про функции из f3.lisp система в этот момент ещё и не подозревает.

finalization

отсутствует, хотя можно задавать действия на уровне системы при выгрузке с помощью lispworks:define-action

Что должно входить в файл *.lisp

В файле *.lisp обязательно должна быть (в начале) форма (in-package :имяПространстваИмён), которая задаёт пространство имён этого файла. Само пространство имён может быть определено в другом месте, но иногда оно определяется в самом файле. Тогда форма (in-package ...) должна идти после определения пакета, а само определение рекомендуется делать с помощью def-merge-packages::! с :always t.

Также должна быть форма (in-readtable :buddens-readtable-a), причём, символ EDITOR-HINTS.NAMED-READTABLES:IN-READTABLE) должен быть доступен в данном пространстве имён. Желательна также форма (asdf::of-system :система).

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

Файл *.asd здесь не описан, см. примеры.

Динамическая разработка, основные сценарии

Динамическая разработка есть в SQL (create/alter/drop table, procedure и т.п.). В Лиспе она по сути, такая же по смыслу – можно менять отдельные объекты.

Работа в listener (REPL)

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

Разработка по одной форме в файле

В listener хорошо отлаживать фрагменты программы и программировать факториалы. При "настоящем" программировании, для нового логически связанного фрагмента программы сразу создают новый файл. Файл сначала создаётся согласно пункту «что должно входить в файл *.lisp», см. чуть выше. Далее в него вносят определения функций и с помощью команды Compile defun (f7) компилируют их по одному. Смотрят на предупреждения. Каждое определение можно проверить в листенере, при этом желательно предварительно сделать в нём команду (in-package :тотЖеПакет), чтобы было меньше путаницы. При перекомпиляции сущности ведут себя так:

Сущность

Как ведёт себя

функция (defun)

сразу вступает в действие. Все другие функции будут вызывать её. Если функция находится на стеке, продолжит выполняться старое тело до выхода. Рекурсивные вызовы будут вызывать новое тело. Но! Всё это может не работать при включенных оптимизациях.

глобальная переменная 1 (defparameter)

присваивается новое значение

глобальная переменная 2 (defvar)

если переменная уже была переменной, заданной с помощью defvar, НЕ присваивается новое значение, выдаётся предупреждение

defconstant

лучше так не делать. Если очень надо, сделать unintern символу из всех пакетов, а затем перекомпилировать весь зависимый код

defstruct

действует новое определение. Экземпляры протухают

defclass

действует новое определение. Экземпляры остаются с новым определением, но конструктор не вызывается заново.

defmacro

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

deftype

не знаю. Безопасным будет перекомпилировать зависимый код

defЧтоТо

Любой человек может создать своё defЧтоТо, см. определение этого defЧтоТо

пространство имён defpackage

сразу вступает в действие новое определение. Старые внутренние символы остаются на месте.

система asdf (defsystem)

её не надо перекомпилировать. Вместо этого попробуйте её перезагрузить (см. мануал asdf или просто asdf::! ), внимательно глядя на *standard-output* В сложных случаях проще всего стереть все *.ofasl и перезапустить лисп-систему.

Перекомпиляция файла целиком

Тоже вполне допустимая практика, если меняли в нескольких местах. Она покажет такие ошибки, как неверно закрытые скобки и дважды определённую в одном файле функцию или переменную. В случае ошибки чтения можно попробовать в отладчике написать :e <Enter>, окрестности ошибки раскрасятся в разные цвета. Также можно щелкать на ворнинги какой-то кнопкой мыши и, если повезёт, получится увидеть исходник.

Подводные камни динамической разработки и как их обойти

Некоторые из них известны в SQL. Только один пример. Мы создали функцию bar, к-рая вызывает функцию foo. Потом решили переименовать функцию foo, взяли её определение, поменяли в ней имя foo на нормальноеИмя и перекомпилировали, а в функции bar - забыли поменять. Всё хорошо, но функция foo тоже существует в пространстве имён, и при перекомпиляции функции bar никто нам не скажет, что она всё ещё ссылается на foo, а не на нормальноеИмя. Проблема всплывёт только при полной пересборке (а это может быть и через неделю).

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

Не буду объяснять их все, но вот общие рекомендации:

1. Макросы, типы данных и глобальные переменные лучше выделить в отдельный файл, примерно так же, как это делается в C.

2. Желательно дробить программу на как можно более мелкие пакеты, для этого нужно пользоваться def-merge-packages::! , т.к. у defpackage хватает граблей, связанных с одноимёнными экспортируемыми символами в разных пакетах и др.

3. Нужно стараться удалять сущности, которые больше не нужны. Любая сущность, имя которой есть хотя бы в одном пространстве имён, пребудет во веки веков (до выхода из программы), даже если на неё нет других ссылок. Поэтому, для удаления сущности нужно удалить её имя из пространства имён. В целом удаление символа из пространства имён делается с помощью unintern, но это не сработает, если имя содержится в нескольких пространствах имён или если оно пришло через наследование пространств имён (:use). Проверяйте результат unintern с помощью средства поиска символов и не забывайте, что могут быть одноимённые, но различные символы в разных пакетах.  Идентичность символов проверяется с помощью eq.

4. В трудной ситуации можно попробовать удалить (delete-package) пространство имён целиком. Сначала нужно перейти в другое пр-во имён в listener-е и закрыть файлы этого пакета в редакторе, после чего выполнить delete-package и перекомпилировать все файлы фрагмента, начиная с файла определения пакета. Вряд ли стоит пытаться это делать, если на данный пакет уже ссылаются другие, в этом случае см. следующую рекомендацию.

5. Полная пересборка системы подразумевает удаление всех ofasl - файлов, созданных при пересборке (нужно иметь функцию или bat-файл, к-рый это делает) и запускать полную пересборку программы в свежем экземпляре среды. Иногда сборочный скрипт не включает в себя сборку особо сложных библиотек. Например, автор почти никогда не пересобирает ap5. Соответствующие ofasl-ы нужно сохранить.

6. asdf работает очень плохо (вроде 2-я версия стала лучше, но точно не знаю). Не полагайтесь на задание зависимостей внутри системы, используйте в системе ключ :serial, а также есть asdf::undefsystem , который сбрасывает кеш asdf для данной системы и который всегда лучше вызвать, если что-то серьёзно поменялось.

Встроенные типы данных

Символы

Символ - это структура данных, которая в обычном компиляторе соответствует идентификатору. В Delphi, символы существуют только во время компиляции и в сессии отладчика. В Лиспе символ доступен программисту. Символ можно создавать во время исполнения и включать в пространства имён. У символа есть значение (это - значение глобальной переменной с таким именем),  значение - функция (функция с таким именем), которая может быть изменена - на этом основана динамическая разработка. Также символ может означать тип, класс, макрос. К символу можно добавлять свойства, доступ к которым происходит с помощью get. Но самое главное - у символа есть имя, и когда читатель встречает в тексте нечто, что не является строкой или числом, он считает, что это символ и возвращает его. Если символа с таким именем нет в текущем пространстве имён, читатель создаёт такой символ и возвращает.

Регистр символов

При чтении, согласно стандарту, имя приводится к верхнему регистру и печатается в верхнем регистре. Чтобы этого избежать, имя нужно писать как |CamelCase|. В :buddens-readtable-a, если у символа есть латинские буквы в различном регистре, например, в имени CamelCase, то он не приводится к верхнему регистру. Также настроены переменные конфигурации чтения таким образом, что имена и вид на печати символов связаны согласно таблице:

Вводим

На печати

Имя символа

sym

sym

"SYM"

SYM

sym

"SYM"

Sym

Sym

"Sym"

|sym|

|sym|

"sym"

Чтобы узнать имя символа и не запутаться в преобразованиях, можно использовать (string символ).

Списки

Список, как уже ранее отмечалось - это односвязный список, построенный из объектов типа cons. В cons два поля - car и cdr. car указывает на элемент списка,cdr - на хвост. У последнего элемента cdr = nil. Несмотря на то, что односвязный список далеко не всегда оптимален по быстродействию, его часто используют для хранения последовательностей объектов, множеств, отображений (словарей). Список можно примерно считать аналогичным TList, но он гибче, поскольку два списка могут иметь общий "хвост". Одинаковый код обработки списков на Лиспе оказывается гораздо лаконичнее соответствующего кода на Дельфи, поскольку:

1. В лиспе можно одним оператором создать и заполнить вложенный список произвольных значений.

2. Легко складывать (конкатенировать) списки, сортировать, искать по произвольному ключу, удалять сразу много значений, делать выборки, осуществлять теоретико-множественные операции.

3. Не нужно следить за памятью.

4. Не нужно создавать специальные типы данных для хранения групп значений  - можно использовать вложенные списки.

5. Список объектов, которые имеют внешние текстовое представление, за одно действие может быть введён/выведен в/из поток(а). В частности, список всегда легко напечатать и обозреть его содержимое.

Как правило, списки всё же используются для относительно небольших объёмов данных.

Массивы и строки

Строки являются одномерными массивами букв. В лиспе есть одномерные и многомерные массивы. Многие функции, работающие для списков, работают и для одномерных массивов. Массивы могут быть изменяемого размера. Принципиальное отличие от списков - в том, что операция добавления и вырезания элемента в массивах могут быть медленнее, а операция доступа по номеру - быстрее. Массивы могут быть слабыми, т.е. не мешать объекту подвергнуться сборке мусора, если на него нет других, "сильных" ссылок. Массивы,как и списки, могут быть напечатаны или прочитаны из потока за одно действие.

Хеш-таблицы

Хеш-таблицы - это эффективная реализация словаря (отображения). Ключём может быть строка, число или произвольный объект. Ключи могут сравниваться по идентичности объекта (eq-хеш-таблицы) или по его содержимому (остальные). Хеш-таблица может быть слабой, т.е. не мешать объекту подвергнуться сборке мусора, если на него нет других, "сильных" ссылок. Используя объект в качестве слабого ключа в хеш-таблице, можно, по сути, добавить новое поле к уже существующему объекту, который мы не можем поменять. Например, таким объектом может быть структура из начинки реализации Лиспа. Хеш-таблицы могут разделяться между нитями, но их можно создавать и для использования в одной нити, в этом случае они работают быстрее.

Числа

Числа бывают целые с произвольной точностью, дроби и с плавающей точкой. Результат операций над целыми числами является целым или дробью, для приведения к плавающей точке нужно использовать (coerce 1 'double-float). Отличаются single-float и double-float, это может вызвать путаницу.

Полиморфные (родовые) функции, структуры и классы

Родовые функции

В Дельфи есть виртуальные функции. Такая функция объявляется в одном (базовом) классе и может перекрываться для его наследников.

В зависимости от типа объекта, для которого вызывается функция, будет реально исполнен разный код.

Также в Delphi есть перегруженные (overload) функции, которые исполняются по-разному в зависимости от типов нескольких аргументов.

Но, в отличие от виртуальных функций, выбор той или иной из набора перегруженных функций определяется типом переменной, а не типом объекта. Если переменная имеет тип TObject, а хранится в ней TComponent, то будет вызвана перегруженная функция для TObject.

В Лиспе также есть полиморфные, или родовые функции. Они похожи на перегруженные тем, что исполняемый код зависит от типов всех параметров, и похожи на виртуальные функции тем, что тип параметра определяется не по типам переменных, а по типам объектов, во время исполнения. Подобно виртуальным методам Delphi, где можно вызвать inherited, в Лиспе можно вызвать "родительский" метод с помощью (call-next-method).

Основное отличие родовой функции от метода Delphi - она не принадлежит объекту! Т.е., вместо object.method(arg1,arg2) пишем (method object arg1 arg2). Внутри определения метода родовой функции нет неявного self, обращаться к полям и методам object нужно по полному имени с упоминанием object. Это может быть неудобным, но такова наша жизнь.

Поскольку код определяется типами нескольких параметров, а классы имеют множественное наследование, в Лиспе нет однозначного понятия "родительский". Реально исполняемый код зависит от типов фактических параметров по довольно сложному алгоритму. Полное описание алгоритма можно найти в Practical Common Lisp. Мы рассмотрим только простые частные случаи.

Основные методы с одним типизированным параметром

У методов может быть квалификатор. Основной метод – это метод, у которого нет квалификатора. Будем для простоты считать, что для каждого метода функции задан тип только одного аргумента – первого. Этот случай соответствует не перегруженному виртуальному методу в Дельфи. Всё работает совершенно аналогично: вызывается метод самого класса или метод ближайшего предка, а call-next-metod работает так же, как inherited.

Методы со спецификатором eql

Вместо спецификации типа может быть задан спецификатор sql. Это позволяет определять метод не для типов, а для индивидуальных объектов.

Методы before, after, around

Можно задать метод с квалификаторами :before, :after, :around. Методы before и after аналогичны триггерам before или after update. Методы around охватывают вызов "вокруг", и позволяют полностью его подменить. Рассмотрим случай функции с одним типизированным аргументом. Методы :around выполняются в порядке от частного – к общему, т.е. от eql, к классу объекта к предкам. При этом, если где-то не вызвать call-next-method, то выполнение более "общих" методов (как обычных, так и before,after,around) не будет происходить.

Методы :before аналогичны триггеру before update, они выполняются в порядке от частного к общему, изнутри вызова around, но до вызова "основных" методов, call-next-method вызывать не нужно. Методы :after выполняются после основных, внутри around, в порядке от частного к общему.

Структуры

Структура по своим возможностям примерно соответствует классу в Delphi, если не считать свойств (properties) и ограничения доступа - этого в Лиспе нет. Пример определения структуры с тремя полями:

(defstruct мояСтруктура поле1 поле2 поле3)

Для такой структуры будет автоматически сгенерирован конструктор make-мояСтруктура и три функции для доступа к полям, мояСтруктура-поле1,2,3 от одного аргумента.

Можно задавать для структур типы полей.

Каждая структура является классом. Структура может "включать" в себя другую структуру (родитель) с помощью опции :include, (см. помощь для defstruct), тогда она становится подклассом родителя в иерархии классов. Поскольку для структур можно определять родовые функции, можно сделать вывод, что структуры+родовые функции примерно соответствуют классам Дельфи.

Конструктор

Структура пассивна, это - просто набор данных. У неё нет конструктора, его нужно эмулировать вызовом какой-то функции, написанной пользователем.

Деструктор

Деструкторы в лиспе вообще не водятся. Хотя можно назначить действие на сборку мусора, (hcl:add-special-free-action), но неизвестно, когда это действие будет осуществлено, и возможности этого действия весьма ограничены, т.к. оно вызывается в очень специфических условиях. Поэтому, если объект должен быть явно «удалён», например, закрыт поток, нужно добавить в объект поле, характеризующее его состояние, в деструкторе закрывать то, что надо, затем ставить в поле состояния признак, что объект «убит» и особым образом обрабатывать такие «убитые» объекты. Действие при сборке мусора, по хорошему, должно лишь проверять, что объект при сборке мусора был закрыт должным образом, и сообщать об ошибочной ситуации, если что-то не так.

Крышечный синтаксис

Стандарт Лиспа подразумевает довольно многословный синтаксис обращения к полям структуры. Крышечный синтаксис позволяет сделать обращения к полям структур почти столь же лаконичными, как в Delphi. Однако, это будет работать только для цепочек типа a.b.c и не будет работать для a.b(args).c. Пример см. в пункте про управляющие конструкции.

Ложка дёгтя

При переопределении структуры все её экземпляры становятся инвалидными.

Классы

Классами автор пользуется мало. Основное их преимущество перед структурами – их можно переопределять «на лету», так, что все экземпляры сохраняются. Для классов доступно множественное наследование, могут быть определены конструкторы. Для классов функция  печати по умолчанию очень убогая, нужно пользоваться инспектором или писать нужную функцию. Недостатком классов можно считать то, что родовые функции являются аналогом виртуальных, что сказывается на производительности. Впрочем, ничто не мешает написать обычную функцию, оперирующую над экземплярами классов - это будет аналогично статическим методам. Синтаксис определения класса могуч и многословен. Повторюсь, родовые функции можно определять не только над классами, но и над структурами и встроенными типами, так что без классов можно обходиться в большом количестве случаев.

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

Проблема именования полей и методов

В Delphi каждый класс фактически определяет пространство имён. Если два класса отличны, то, T1.func() и T2.func() - тоже отличны. В лиспе это - не так. Пространства имён ортогональны иерархии классов. Отсюда возникает вопрос, как назвать func в Лиспе? Её можно назвать T1-func, но тогда при доступе всегда нужно будет писать имя типа - это неудобно. Кроме того, если у T1 появится потомок T1Child, нужно будет либо писать новую функцию T1Child-member, либо писать (T1-member instance), где instance будет типа T1Child, что тоже ведёт к путанице. Приходится учитывать эту особенность при и особенно тяжело здесь приходится с самыми распространёнными словами, такими, как add,push и т.п. Один из вариантов решения - создавать иерархии пространств имён, связанные с иерархиями классов. В любом случае, в среднем, получается более многословно, чем в Delphi и с этим надо смириться.

В случае структур ситуация упрощается, (только для полей). Если в стурктуре T1 есть поле fld, автоматически создаётся функция доступа T1-fld. Если создаётся наследник T1Child, то к полю fld наследника можно обращаться и через T1-fld, и через T1Child-fld. Крышечный синтаксис позволяет обращаться к полям структур почти так же лаконично, как в Delphi: instance^fld, где instance может быть T1 или T1Child. Для достижения хорошей производительности тип переменной должен быть введён конструкцией with-the1.

 

Пример реализации иерархии классов на структурах (НЕ ПРОВЕРЕНО!)

unit relatives;

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

type TFather=class

  s:string; // (в лиспе всё - public)

  constructor Init;

  procedure SetString(iS:string)

 

 

 

  function getString:string; virtual;

  end;

 

type TChild=class(TFather)

  function getString:string; override;

  end;

 

implementation

 

procedure TFather.setString(iS:string);

{ статический метод}

begin

s:=iS;

end;

 

 

 

function TFather.getString:string;

begin

result:=s;

end;

 

function TChild.getString:string;

begin

result:=s+s;

end;

 

end.

<EOF>

 

unit relativesApp;

uses relatives;

 

 

 

 

 

 

var f:TFather;ch:TChild;

begin

f:=TFather.Create;

ch:=TFather.Create;

 

f.s;

 

 

 

 

ch.s;

f.setString('abc'); // вызов статического метода

ch.setString('abc');

 

f.getString; // вызов виртуального метода

ch.getString;

end.

; файл relatives/package.lisp весь пример - синий

(def-merge-packages::! :relatives

  (:use :cl :budden-tools)

  (:export "relatives:TFather

   relatives:TFather-P    ; всё это надо прописать

   relatives:make-TFather ; руками

   relatives:TFather-S    ; хотя можно автоматизировать

   relatives:TFather-Init

   relatives:TFather-SetString

   relatives:GetString

   relatives:TChild

   relatives:make-TChild

   relatives:TChild-P

   relatives:TChild-S

   "

  )

  (:custom-token-parsers budden-tools::convert-carat-to-^)

   )

<EOF>

;; файл relatives/vars-and-macros.lisp  

(in-package :relatives)(in-readtable :buddens-readtable-a)

 

; реализация на структурах.

(defstruct TFather

  S)

 

; статический метод имитируем обычной функцией.

 

 

; объявляется 1 раз на иерархию

(defgeneric GetString (self))

 

 

(defstruct (TChild (:include TFather)))

 

<EOF>

 

 

;; файл relatives/implementation.lisp

(in-package :relatives)(in-readtable :buddens-readtable-a)

(defun TFather-SetString (self iS) ; статический "метод"  

             ; изображаем обычной функцией и даём имя

             ; класса, где он определён.

   (proga

     (with-the1 self TFather self)

     (setf self^S iS) ; проверки типа нет

     ))

 

(defmethod GetString ((self TFather))

 

  (self^S)

  )

 

(defmethod GetString ((self TChild))

 

  (str++ self^S self^S)

)

 

 

<EOF>

 

;; файл relatives-app.lisp

(def-merge-packages::! :relatives-app

  (:use :cl :budden-tools :relatives-app)

  (:custom-token-parsers budden-tools::convert-carat-to-^)

  )

(in-package :relatives-app)

(in-readtable :buddens-readtable-a)

 

(defparameter *f* nil)) ; глоб. переменные не типизированы

(defparameter *ch* nil)

(setf *f* (TFather-Init (make-TFather)))

(setf *ch* (TFather-Init (make-TFather)))

 

(print *f*^S) ; неэффективно, т.к. не типиз. переменная.

; Эквивалентный эффективный вариант

; (with-the1 f TFather *f* f^S) или просто (TFather-S *f*)

 

(print *ch*^S)

(TFather-SetString *f* "abc")

(TFather-SetString *ch* "abc") 

 

(GetString *f*)

(GetString *ch*)

eval и макросы

Eval

Eval предназначен для динамической генерации и выполнения кода во время выполнения программы. Он аналогичен командам SQL: execute statement, exec, sp_execute_sql и т.п. Общий смысл - код составляется и выполнятся во время выполнения. Разница между SQL и Лиспом состоит в том, что в SQL складывются строки, а в Lisp происходит обработка деревьев. При этом часто используется квазицитирование.

Eval снижает производительность и делает программу менее надёжной, поэтому его можно использовать только там, где это необходимо. Однако, листенер основан именно на eval - он вызывает eval над формами, которые мы вводим.

Макросы

Макросы аналогичны директивам препроцессора #define в C и в то же время аналогичны eval. Отличие от #define состоит в том, что для #define доступны 3-4 операции (арифметические действия над константами, вызов других макросов и две конкатенации). Макрос на лиспе может проводить любой анализ входных данных (своих аргументов), вызывать любые уже присутствующие в образе функции и строить подставляемый код из результата их вызова. Всё это обычно может в интерпретируемых языках eval (execute statement). Отличие макросов от eval состоит в том, что расширение и подстановка макросов обычно происходит на этапе компиляции, поэтому макросы не замедляют выполнение программы.

При определении макросов обычно используется квазицитирование. Пример макроса приведён в пункте try..finally.

Преимущества и недостатки

Преимущество - возможность создания новых конструкций языка с неограниченными возможностями. Например, препроцессор SQL с поиском ошибок во время компиляции, о котором говорилось выше. Недостаток - макросы затрудняют или делают невозможной пошаговую отладку. Степпер предлагает расширять макросы, когда он их встречает, но поствить брекпойнт в нерасширенный макрос не всегда можно. Иногда нелегко найти и ошибку в исходном тексте, полученном в резлультате макрорасширения (для этого нужно использовать macroexpand или трассировать макрос).

Отладка

Три отладчика: консольный, жук и степпер

Консольный и графический отладчик работают координированно над одним и тем же стеком, эта пара является основным средством интерактивной отладки. Степпер используется довольно редко.  Часто применяется логгирование как полезное средство отладки.

Для отладки поместите код в файл

Лисп позволяет определять и компилировать функции прямо в листенере. Одна из основных функций отладчика - показать место в исходнике, где случилась ошибка. Для этого функция должна быть определена в файле, а файл - скомпилирован и загружен (например, с помощью команды меню File/Compile and load). Если в процессе отладки в код вносятся правки, действуйте согласно главе "Динамическая разработка, основные сценарии".  

Изучение состояния программы в отладчике

Просмотр стека. Настройка отображения кадров стека

Можно просматривть стек и переменные в нём. Для переменных можно вызывать инспектор. Также можно просматривать в инспекторе функцию, в которой произошёл останов, с помощью пункта inspect function, в т.ч. её ассемблерный листинг. В меню "view" жука можно выбрать отображение тех или иных кадров стека. По умолчанию отладчик показывает лишь небольшую часть из них.

Инспектор

Инспектор аналогичен инспектору в Delphi - он позволяет ходить по внутренней структуре объекта.

В нормальных условиях инспектор вызывается нажатием иконки с микроскопом в листенере. В этом случае он показывает содержимое последнего результата вычисления. В графическом отладчике - "жуке" инспектор вызывается при двойном щелчке нажатии на любую переменную стека. Для возврата на предыдущий уровень структуры есть кнопка "назад", как в веб-браузере.

Листенер во время отладки

В любое время, листенер позволяет вызывать функции и вычислять значения глобальных переменных. Если Вы находитесь в отладчике (см. "Отладчик очень близко"), то листенер позволяет делать вычисления в контексте текущего кадра стека. Это аналогично окну Evaluate/Modify в отладчике Delphi, но без присущих Delphi ограничений. Например, можно в отладчике определить новую функцию. Если в графическом отладчике ("жуке") Вы выделили одинарным щелчком другой кадр стека, то будут видны переменные этого кадра и над ними можно будет делать вычисления. Но пространство имён при этом не переключается, поэтому может понадобиться писать префикс пространства имён перед пакетом.

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

Разновидности ошибок и их локализация

В Дельфи бывает два вида ошибок по времени их возникновения - ошибки компиляции и ошибки исполнения. В CL жизненный цикл программы устроен гораздо сложнее, поэтому перечислим эти виды и способы их локализации. Напомним порядок обработки каждого файла программы: чтение-компиляция-загрузка компилированного кода-выполнение, причём эти фазы могут происходить в разных образах Лисп-системы.

Ошибки чтения

Они возникают в функции read. Соответственно, если среда говорит об ошибке и неглубоко по стеку Вы видите функцию read, то это - ошибка чтения. В кадре стека ошибки есть переменная system::eargs, и в ней есть поток. Инспектируя поток, можно понять имя файла. Делая read-line этому потоку, можно попробовать прочитать продолжение файла после места ошибки. editor-budden-tools::edit-stream-position открывает файл в том месте, где чтение прервалось. С таблицей чтения buddens-readtable-a можно использовать в отладчике команду :e для вызова специального просмотровщика ошибок чтения, к-рый раскрасит файл в цвета по уровням вложенности. Также можно заполнить файл отладочным выводом: #.(print "уникальная метка") и методом половинного деления найти место, в котором случилась ошибка.

Ошибки компиляции

Во многих случаях среда (в консоли) предлагает edit source where error occured. Также можно установить переменную *compile-print* в t,но это не поможет, если в файле много форм, являющихся макросами, определёнными пользователем. В этом случае, вместо имени функции, среда напечатает "(top-level-form NNN)". Можно попробовать установить трассировку на compiler::process-form. Самый общий способ основан на отладочном выводе: локализуйте ошибку, расставляя в файле формы верхнего уровня (eval-when (:compile-toplevel) (print "уникальная метка")).

Ошибки загрузки

Аналогичным образом, можно искать ошибку с помощью расстановки форм (eval-when (:load-toplevel) (print "уникальная метка"))

Трассировка и отладка print-ами

Трассировка очень удобна, т.к. она показывает параметры и возвращающие значения любого множества функций, отображая глубину вложенности стека отступами. Можно трассировать обычную функцию, родовую функцию или её отдельные методы, а также макросы. Для трассировки обычной ф-ии пишем (trace имя), нажимаем ctrl-alt-t на её имени или выбираем трассировку в контектном меню. Перекомпилировать функцию не нужно. Можно делать  трассировку с (break)  - в этом случае исполнение прервётся при входе. Трассировку методов можно делать через Works/Tools/Generic function browser, выбирая методы и вызывая для каждого из них контекстное меню. Для выключения трассировки всех функций выполняем (untrace). Аналогичным трассировке методом является отладочная печать. Show-expr позволяет выводить само выражение и его значение.

Остановы и пошаговое исполнение

Надёжным способом установки точки прерывания является вставка в нужное место кода вызова (break) c последующей перекомпиляцией функции. При таком способе останова нельзя из режима останова перейти в режим пошагового исполнения. Негусто, но радуемся тому, что можно перекомпилировать по одной функции, поэтому затраты времени на установку (break) невелики.

Теоретически, в коде можно ставить точки останова, нажав на красную кнопку с кружком в начале формы, где хотим остановиться. Эта возможность запускает пошаговый отладчик. Всё бы хорошо, но iterate и proga так меняют код, что эта возможность становится почти везде недоступной - среда не умеет ставить точки останова в коде, подвеграющемся макрорасширению.

Assert, with-the1

Assert означает то же, что в Delphi и им нужно пользоваться как можно более интенсивно. With-the1 сочетает в себе связывание локальной переменной, декларацию типа и его проверку.

Unit-тестирование

Unit-тестирование в лиспе не составляет затруднений благодаря его устройству. Автор пользуется собственным (очень легковесным) средством определения тестов, def-trivial-test::! В серьёзных системах тесты размещаются в отдельных asdf-системах, но автор обычно разрабатывает код стремительно и для экономии сил помещает тесты прямо в исходные тексты; тесты выполняются во время загрузки. Заодно тесты служат примерами использования кода.

Профайлер

Он есть в Lispworks.

Трассировка SQL

Для печати в консоль всех выполнямых sql-запросов надо установить в t глобальную переменную *trace-firebird*.

 

Контрольные вопросы и практические задания

Для практических заданий нужно:

- WindowsXP точно работает)

- Lispworks 6.0 Personal Edition

- http://code.google.com/p/def-symbol-readmacro/ (см. Wiki "Установка и загрузка")

- TeamViewer для демонстраций

 

1. assert, trace, break, макросы, macroexpand, квазицитирование, основной метод, родовая функция, call-next-method, cons, car, cdr, eq, *standard-output*, listener, lambda, defpackage, progn, defmacro, funcall, let, defparameter, defvar, список, переход от закрывающей к открывающей скобке и обратно, удаление списка до или после курсора в буфер обмена, переход на следующий/предыдущий список, print, read, символ, пакет, defun, defgeneric, defmethod, setf, cond, defstruct, defclass, coerce, prin1-to-string, read-from-string, parse-integer, string, format - знать определения всех понятий и зачем это нужно. Про каждое понятие указать 1-2 прочитанных Вами источника, где оно описано (письменно) и быть готовым ответить на вопросы по этим источникам.

 

2. Каким типом обозначается "любой объект".

3. Продемонстрировать через TeamViewer процесс поиска документации на слово eql в IDE Lispworks, а также в Язык Common Lisp Второе издание

3. Создать asdf-систему test3, с пакетом :test3, включающую файл test3.lisp со следующими функциями

 

(defun f1 (x)

   (+ x 1))

 

(defun f2 (y)

   (+ 4 (f1 "4")))

 

функция f2 должна быть экспортируемой.

Вызвать функцию f2 с параметром 5 из Listener.  

 

Продемонстрировать локализацию ошибки с помощью жука. Объяснить, почему она произошла.

 

4. Загрузить swank (SLIME). Используя функцию swank-backend::generic-function-p, вывести список всех методов всех родовых функций, определённых в системе в виде именованных функций. Каждый метод должен входить в список один раз. Использовать list-all-packages, do-symbols, fboundp, symbol-function, remove-duplicates.

 

5. С помощью инспектора просмотреть родовую функцию print-object. Просмотреть любой метод и его ассемблерный листинг (продемонстрировать). Попытаться написать функцию, возвращающую по методу его ассемблерный листинг. См. disassemble, find-method, slot-value. Может не получиться,поэтому не тратьте на это задание более 4 часов. В любом случае, должен получиться какой-то промежуточный результат и его нужно представить в качестве результата выполнения задания.

 

6. Трассировать функцию lispworks-tools::directory-search-internal-search с прерыванием (trace with break).

Вызвать средство поиска в файлах, задать способ поиска "Root and Patterns" и посмотреть, как работает эта функция.

С помощью decorate-function::decorate-function попробовать подставить свой список каталогов вместо введённого пользователем.

 

7. а) Переделать пример из главы Overload, чтобы поиск был с учётом регистра букв.

    б) то же самое, но без учёта регистра букв не только латиницы, но и кириллицы (использовать russian-budden-tools::char-equal-cyr)

 

8. Написать функцию, которая от суммы первого, третьего, ... параметров вычитает сумму второго, четвёртого, ...

С использованием &rest. Внимание! Никакая функция с &rest не должна разрушать свой список параметров - это приводить к непредсказуемым последствиям.

 

9. Реализовать перечислимый тип. Должно получиться так:

>(def-enum-type TWeekDays (Monday Tuesday Wednesday Thursday Friday Saturday Sunday))

TWeekDays

>(typep 'Monday 'TWeekDays)

t

>(ord 'Monday)

0

>(succ 'Monday)

Tuesday

>(GetEnumName 'TWeekDays 0)

Monday

 

Также должна быть конструкция enum-case, аналогичная case, которая допускает в качестве ключей и своего аргумента только символы из перечисления,

 

>(enum-case ('Monday TWeekDays)

    (Monday "Понедельник")

    (t "Другой день"))

"Понедельник"

 

>(enum-case ('Monday TWeekDays)

   (0 "Понедельник")

   (t "Другой день"))

; ошибка во время макрорасширения

 

>(enum-case ('cons TWeekDays)

    (Monday "Понедельник")

    (t "Другой день"))

; ошибка во время выполнения

 

Особенности реализации:

Числовое значение перечисления хранится в property list (списке свойств, http://filonenko-mikhail.github.com/cltl2-doc-ru/clmse54.html#x69-94200010.1) символа, имя свойства - это имя типа перечисления.

 

def-enum-type должно разворачиваться в progn, который:

1. делает (deftype TWeekDays () '(member Monday Tuesday ...))

2. для каждого символа проверяет, что он ещё не является элементом другого перечисления (через перебор списка свойств)

3. назначает в property list числовое значение.

 

Функция Ord должна быть обычной (не родовой) функцией.

 

enum-case должно делать check-type во время компиляции для каждого ключа. Она должна расширяться в конструкцию, которая делает check-type для аргумента и расширяется в case.

 

Будяк Д.В.

 

__________


К началу страницы