Регистрация | Войти
Lisp — программируемый язык программирования
Предыдущая Оглавление

32. Заключение: что дальше ?

Я надеюсь, теперь вы убеждены, что в названии этой книги нет противоречия. Однако вполне вероятно, что есть какая-то область программирования, которая очень практически важна для вас, и которую я совсем не обсудил. Например, я ничего не сказал о том, как разрабатывать графический пользовательский интерфейс (GUI), как связываться с реляционными базами данных, как разбирать XML, или как писать программы, которые являются клиентами различных сетевых протоколов. Также я не обсудил две темы, которые станут важными, когда вы начнете писать реальные приложения на языке Common Lisp: оптимизацию кода и сборку приложений для поставки.

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

Поиск библиотек LISP

В то время как стандартная библиотека функций, типов данных и макросов, поставляемая с языком Common Lisp, достаточно велика, она служит лишь целям общего программирования. Специализированные задачи, такие, как создание графического пользовательского интерфейса (GUI), общение с базами данных и разбор XML требуют библиотек, не обеспечиваемых стандартом этого языка.

Простейшим путем найти библиотеку, делающую что-то нужное вам, может быть просто проверить ее наличие в составе вашей реализации Lisp. Большинство реализаций предоставляет по крайней мере некоторые возможности, не указанные в стандарте языка. Коммерческие поставщики, как правило, особенно усиленно работают над предоставлением дополнительных библиотек для своих реализаций с целью оправдать их стоимость. Например, Franz's Allegro Common Lisp Enterprise Edition поставляется среди прочего с библиотеками для разбора XML, общения по протоколу SOAP, генерирования HTML, взаимодействия с реляционными базами данных и построения графического интерфейса пользователя различными путями. Другая выдающаяся коммерческая реализация, LispWorks, также предоставляет несколько подобных библиотек, включая пользующуюся уважением переносимую библиотеку CAPI, которая может использоваться для разработки GUI-приложений, работающих на любой операционной системе, где есть LispWorks.

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

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

Переносимые библиотеки (переносимость означает либо что они написаны целиком на стандартном Common Lisp, либо что они содержат необходимые макросы чтобы работать с несколькими реализациями) 1) лучше всего искать в Интернете.

Вот три лучших места, откуда можно начать поиски (с обычными предосторожностями, связанными с тем, что все URL устаревают сразу же, как только они напечатаны на бумаге):

  • Common-Lisp.net (http://www.common-lisp.net/) – сайт, размещающий свободные и открытые проекты на Common Lisp, предоставляя средства контроля версий исходного кода, списки рассылки и размещение WEB-документов проектов. В первые полтора года работы сайта было зарегистрировано около сотни проектов.
  • Коллекция открытого кода Common Lisp (The Common Lisp Open Code Collection, CLOCC) (http://clocc.sourceforge.net/) – немного более старая коллекция библиотек свободного ПО, которые, как подразумевается, должны быть переносимы между разными реализациями языка Common Lisp и самодостаточными, то есть не зависящими от каких-то других библиотек, не включенных в эту коллекцию.
  • Cliki (Common Lisp Wiki) (http://www.cliki.net/) – Wiki-сайт, посвященный свободному программному обеспечению на языке Common Lisp. В то время как и любой другой сайт, основанный на Wiki, он может изменяться в любое время, обычно на нем есть довольно много ссылок на библиотеки и реализации Common Lisp с открытым кодом. Система редактирования документов, на которой работает сайт, и давшая ему имя, также написана на языке Common Lisp.

Пользователи Linux, работающие на системах Debian или Gentoo, также могут легко устанавливать постоянно растущее число библиотек для языка Lisp, которые распространяются с помощью средств распространения и установки этих систем: apt-get на Debian и emerge на Gentoo.

Я не буду сейчас рекомендовать какие-то конкретные библиотеки, поскольку ситуация с ними меняется каждый день – после многих лет зависти к библиотекам языков Perl, Python и Java, программисты на Common Lisp в последние пару лет приняли вызов к созданию такого набора библиотек, которого заслуживает Common Lisp, и коммерческих, и с открытым кодом.

Одна из областей, в которой в последнее время было много активности – это фронт разработки графического интерфейса приложений. В отличие от Java и C#, но как и в языках Perl, Python и C, в языке Common Lisp не существует единственного пути для разработки графического интерфейса. Способ разработки зависит от реализации Common Lisp, с которой вы работаете, а также от операционной системы, которую вы хотите поддерживать.

Коммерческие реализации Common Lisp обычно обеспечивают какой-то способ разработки графического интерфейса для платформ, на которых они работают. В дополнение к этому, LispWork предоставляет библиотеку CAPI, уже упоминавшуюся, для разработки переносимых графических приложений.

Из программного обеспечения с открытым кодом у вас есть несколько возможных вариантов. На системах Unix вы можете писать низкоуровневые приложения для системы X Windows используя библиотеку CLX, реализацию X Windows протокола на чистом языке Common Lisp, примерно такая же, как xlib для языка C. Или вы можете использовать различные обертки для высокоуровневых API и библиотек, таких как GTK или Tk, также, как вы можете делать это в языках Perl или Python.

Или если вы ищите чего-то совсем необычного, вы можете посмотреть на библиотеку Common Lisp Interface Manager (CLIM). Будучи наследником графической библиотеки символических LISP-машин, CLIM является мощным, но сложным средством. Хотя многие коммерческие реализации языка LISP поддерживают его, не похоже что он очень сильно использовался. Но в последние несколько лет новая реализация CLIM с открытым кодом, McCLIM, набирает обороты (она располагается на сайте Common-Lisp.net), так что возможно мы на грани нового расцвета этой библиотеки.

Взаимодействие с другими языками программирования

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

Стандарт языка не дает механизма для вызова из кода на языке LISP кода, написанного на другом языке программирования, и не требует, чтобы реализация языка предоставляла такие возможности. Но в наше время почти все реализации поддерживают то, что называется Foreign Function Interface, или кратко – FFI2).

Основная задача FFI – позволить вам дать языку LISP достаточно информации для того, чтобы прилинковать чужой код, написанный не на LISP. Таким образом, если вы собираетесь вызывать функции из какой-то библиотеки для языка C, вам нужно рассказать языку LISP о том, как транслировать объекты LISP, передаваемые этой функции, в типы данных языка C, а значение, возвращаемое функцией, – обратно в объекты языка LISP. Однако каждая реализация предоставляет свой собственный механизм FFI, со своими отличными от других возможностями и синтаксисом. Некоторые реализации FFI позволяют делать функции обратного вызова (callback functions), другие - нет. Библиотека Universal Foreign Function Interface (UFFI) предоставляет слой для переносимости приложений, написанных с использованием FFI, доступный на более полудюжины реализаций Common Lisp. Библиотека определяет макросы, которые раскрываются в вызовы соответствующих функций интерфейса FFI данной реализации. Библиотека UFFI применяет подход "наименьшего общего знаменателя", то есть она не может использовать преимущества всех возможностей разных реализаций FFI, но все же обеспечивает хороший способ построения оберток API для языка С3).

Сделать, чтоб работало, сделать, чтоб работало правильно, сделать, чтоб работало быстро

Как уже было сказано много раз, по разным сведениям, Дональдом Кнутом, Чарльзом Хоаром и Эдсгером Дейкстрой, преждевременная оптимизация является первопричиной всех зол4). Common LISP - замечательный язык в этом отношении, если вы хотите следовать этой практике и при этом вам нужна высокая производительность. Эта новость может быть для вас сюрпризом, если вы уже слышали традиционное мнение о том, что LISP – медленный язык. В ранние года языка LISP, когда компьютеры еще программировали с помощью перфокарт, этот язык с его высокоуровневыми чертами был обречен быть медленнее, чем его конкуренты, а именно, ассемблер и Фортран. Но это было давно. В то же время LISP использовался для всего, от создания сложных систем искусственного интеллекта, до написания операционных систем, и было проделано много работы, чтобы выяснить, как компилировать LISP в эффективный код. В этом разделе мы поговорим о некоторых из причин, почему Common LISP является прекрасным языком для написания высокопроизводительных программ, и о том, как это достигается.

Первой причиной того, что LISP является прекрасным языком для написания высокопроизводительного кода, является, как ни парадоксально, динамический характер программирования на этом языке – та самая вещь, которая сначала мешала довести производительность программ на языке LISP до уровней, достигнутых компиляторами языка Фортран. Причина того, что динамичность LISP-а упрощает создание высокопроизводительного кода, заключается в том, что первый шаг к эффективному коду – это всегда поиск правильных алгоритмов и структур данных.

Динамические свойства языка LISP сохраняют код гибким, что упрощает опробование разных подходов. Имея ограниченное время на создание программы, вы, в конечном итоге, более вероятно придете к более высокопроизводительной версии, если не будете проводить много времени, забираясь в тупики и выбираясь из них обратно. В языке Common LISP вы можете опробовать идею, увидеть, что она ведет в никуда, и двигаться дальше, не тратя много времени убеждая компилятор, что ваш код все же достоин того, чтобы он наконец запустился, и ожидая, когда же он наконец закончит его компилировать. Вы можете написать простую, но эффективную версию функции – набросок кода –, чтобы определить, работает ли ваш основной код правильно, а затем заменить эту функцию на более сложную и более эффективную ее реализацию, если все хорошо. Но если общий подход к решению оказался с изъяном, то вы не потратите кучу времени на отладку функции, которая в конечном итоге не нужна, что означает, что у вас остается больше времени для поиска лучшего подхода к решению задачи.

Следующая причина того, что Common LISP является хорошим языком для разработки высокопроизводительного программного обеспечения, заключается в том, что большинство реализаций Common LISP обладают зрелыми компиляторами, которые генерируют достаточно эффективный машинный код. Мы остановимся сейчас на том, как помочь компиляторам сгенерировать код, который был бы сравним с кодом, генерируемым компиляторами языка C, но эти реализации и так уже немного быстрее, чем те языки программирования, реализации которых менее зрелы, и которые используют более простые компиляторы или интерпретаторы. Также, поскольку компилятор LISP доступен во время выполнения программы, программист на LISP имеет некие возможности, которые было бы трудно эмулировать в других языках – ваша программа может генерировать код на LISP во время выполнения, который затем может быть скомпилирован в машинный код и запущен. Если сгенерированный код должен работать много раз, то это – большой выигрыш. Или, даже без использования компилятора во время выполнения, замыкания дают вам другой способ объединять код и данные времени выполнения. Например, библиотека регулярных выражений CL-PPCRE, работающая под CMUCL, быстрее, чем библиотека регулярных выражений языка Perl, на некоторых тестах, несмотря даже на то, что библиотека языка Perl написана на высоко оптимизированном языке C. Это вероятно происходит от того, что в Perl регулярные выражения транслируется в байт-код, который затем интерпретируется средствами поддержки регулярных выражений Perl, в то время как библиотека CL-PPCRE транслирует регулярные выражения в дерево скомпилированных функций, использующих замыкание, (FIXME closures?), которые вызывают друг друга средствами нормальных вызовов функций.

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

Есть несколько подходов к профилированию. Стандарт языка предоставляет несколько простейших средств измерения времени выполнения каких-то форм. В частности, макроcом TIME можно обернуть любую форму и он вернет возвращаемое формой значение, напечатав сообщение в поток *TRACE_OUTPUT* о том, как долго выполнялась форма, и сколько памяти она использовала. Конкретный вид сообщения определяется конкретной реализацией.

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

(defun foo ()
(bar)
(baz))

Если вы хотите увидеть, какая из функций работает дольше, вы можете изменить определение функции таким образом:

(defun foo ()
(time (bar))
(time (baz)))

Теперь вы можете вызвать foo, и LISP напечатает два отчета, один для bar, другой для baz. Формат отчета зависит от реализации, вот как это выглядит в Allegro Common Lisp:

CL-USER> (foo)
; cpu time (non-gc) 60 msec user, 0 msec system
; cpu time (gc)     0 msec user, 0 msec system
; cpu time (total)  60 msec user, 0 msec system
; real time  105 msec
; space allocation:
;  24,172 cons cells, 1,696 other bytes, 0 static bytes
; cpu time (non-gc) 540 msec user, 10 msec system
; cpu time (gc)     170 msec user, 0 msec system
; cpu time (total)  710 msec user, 10 msec system
; real time  1,046 msec
; space allocation:
;  270,172 cons cells, 1,696 other bytes, 0 static bytes

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

(defmacro labeled-time (form)
  `(progn
    (format *trace-output* "~2&~a" ',form)
    (time ,form)
)
)

Если вы замените TIME на labeled-time в foo, вы увидете следующий вывод:

CL-USER> (foo)

(BAR)
; cpu time (non-gc) 60 msec user, 0 msec system
; cpu time (gc)     0 msec user, 0 msec system
; cpu time (total)  60 msec user, 0 msec system
; real time  131 msec
; space allocation:
;  24,172 cons cells, 1,696 other bytes, 0 static bytes

(BAZ)
; cpu time (non-gc) 490 msec user, 0 msec system
; cpu time (gc)     190 msec user, 10 msec system
; cpu time (total)  680 msec user, 10 msec system
; real time  1,088 msec
; space allocation:
;  270,172 cons cells, 1,696 other bytes, 0 static bytes

С этой формой отчета сразу ясно, что большее время выполнения foo тратится в baz.

Конечно, вывод от TIME становится немного неудобным, если форма, которую вы хотите профилировать, вызывается последовательно много раз. Вы можете создать свои средства измерения, используя функции GET-INTERNAL-REAL-TIME и GET-INTERNAL-RUN-TIME, которые возвращают число, увеличиваемое на величину константы INTERNAL-TIME-UNITS-PER-SECOND каждую секунду.

GET-INTERNAL-REAL-TIME измеряет абсолютное время, реальное время, прошедшее между событиями, в то время как GET-INTERNAL-RUN-TIME дает некое специфичное для конкретной реализации значение, равное времени, которое Лисп-программа тратит реально на выполнение именно пользовательского кода, исключая внутренние затраты Лисп-машины на поддержку программы, такие, как выделение памяти и сборка мусора.

Вот достаточно простой, но полезный вспомогательный инструмент профилирования, построенный с помощью нескольких макросов и функции GET-INTERNAL-RUN-TIME:

(defparameter *timing-data* ())

(defmacro with-timing (label &body body)
  (with-gensyms (start)
    `(let ((,start (get-internal-run-time)))
      (unwind-protect (progn ,@body)
        (push (list ',label ,start (get-internal-run-time)) *timing-data*)
)
)
)
)


(defun clear-timing-data ()
  (setf *timing-data* ()))


(defun show-timing-data ()
  (loop for (label time count time-per %-of-total) in (compile-timing-data) do
       (format t "~3d% ~a: ~d ticks over ~d calls for ~d per.~%"
               %-of-total label time count time-per
)
)
)


(defun compile-timing-data ()
  (loop with timing-table = (make-hash-table)
     with count-table = (make-hash-table)
     for (label start end) in *timing-data*
     for time = (- end start)
     summing time into total
     do
       (incf (gethash label timing-table 0) time)
       (incf (gethash label count-table 0))
     finally
       (return
         (sort
          (loop for label being the hash-keys in timing-table collect
               (let  ((time (gethash label timing-table))
                      (count (gethash label count-table))
)

                 (list label time count (round (/ time count)) (round (* 100 (/ time total))))
)
)

          #'> :key #'fifth
)
)
)
)

Этот профайлер позволяет вам обернуть вызов любой формы в макрос with-timing. Каждый раз, когда форма будет выполняться, время начала и конца выполнения будет записываться в список, связываясь с меткой, которую вы указываете. Функция show-timing-data выводит таблицу, в которой показано, как много времени было потрачено в различно помеченных секциях кода, следующим образом:

CL-USER> (show-timing-data)
84% BAR: 650 ticks over 2 calls for 325 per.
16% FOO: 120 ticks over 5 calls for 24 per.
NIL

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

Как только вы нашли узкое место в вашем коде, вы начинаете оптимизацию. Первое, что вам следует попробовать, – это, конечно, попытаться найти более эффективный базовый алгоритм, это всегда приносит большую выгоду FIXME (that's where the big gains are to be had). Но если предположить, что вы уже используете эффективный алгоритм, то тогда как раз время начинать рихтовать код, то есть оптимизировать код, чтобы он не делал абсолютно ничего, кроме необходимой работы.

Главное средство при этом – дополнительные объявления LISP. Основная идея объявлений в языке LISP в том, что они дают компилятору информацию, которую он может использовать различным образом, чтобы генерировать более хороший код.

Например, рассмотрим простую функцию:

(defun add (x y) (+ x y))

Как я упоминал в главе 10, если вы сравните производительность этой LISP-функции с вроде бы эквивалентной функцией на языке C,

int add (int x, int y) { return x + y; }

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

Это происходит потому, что версия функции на Common Lisp выполняет гораздо больше всего – компилятор Common LISP даже не знает, что значения x и y являются числами, и таким образом вынужден сгенерирвать код для проверки типов во время выполнения. И как только он определил, что это - числа, он должен также определить, какого типа эти числа - целые, рациональные, с плавающей точкой или комплексные, и перенаправить вызов соответствующей вспомогательной процедуре, обрабатывающей соответствующие типы. И даже если x и y являются целыми числами, то процедура сложения должна позаботиться о том, что результат сложения может быть слишком большим для представления в FIXNUM-числе, числе, которое может быть представлено в одном машинном слове, и, таким образом, должна выделить число типа BIGNUM.

В языке C, с другой стороны, поскольку типы всех переменных объявлены, компилятор знает точно, какой тип значений будет храниться в x и y. И, поскольку арифметика языка C просто вызывает переполнение, когда результат сложения слишком большой, чтобы быть представленным в возвращаемом типе, в функции нет ни проверки на переполнение, ни выделения длинного целого значения, если результат не вмещается в машинное слово

Таким образом, поскольку поведение кода на Common Lisp гораздо более близко к математической корректности, версия на языке C, возможно, может быть скомпилирована в одну или две машинных инструкции. Но если вы пожелаете дать компилятору Common Lisp ту же информацию, которую имеет компилятор языка C о типах аргументов и возвращаемом значении, и принять некоторые компромисы, подобные используемым в языке C, относительно общности кода и проверки ошибок, функция на Common Lisp также может компилироваться в пару машинных инструкций.

Именно для этого и служат объявления. Главная польза объявлений в том, чтобы сказать компилятору о типах переменных и других выражений. Например, вы можете указать компилятору, что складываемые аргументы являются FIXNUM-значениями, написав функцию следующим образом:

(defun add (x y)
(declare (fixnum x y))
(+ x y))

Выражение DECLARE не является формой языка Common LISP, это – часть синтаксиса макроса DEFUN, где оно должно быть указано до тела функции, если оно используется5). Данное объявление объявляет, что аргументы функции x и y будут всегда FIXNUM-значениями. Другими словами, это обещание компилятору, и компилятору разрешено генерировать код в предположении, что все, что вы пообещали ему, будет истинным.

Чтобы объявить тип возвращаемого значения, вы можете обернуть основное выражение функции (+ x y) в специальный оператор THE. Этот оператор принимает спецификатор типа, такой как FIXNUM, и какую-то форму, и говорит компилятору, что эта форма будет возвращать значение указанного типа. Таким образом, чтобы дать компилятору Common Lisp всю ту же информацию, которую имеет компилятор языка C, вы можете написать следующее:

(defun add (x y)
(declare (fixnum x y))
(the fixnum (+ x y)))

Однако даже эта версия нуждается в еще одном объявлении, чтоб дать компилятору Common Lisp те же разрешения, что есть и у компилятора языка C, генерировать быстрый, но опасный код. Объявление OPTIMIZE используется для того, чтобы указать компилятору как распределить свое внимание между пятью целями:

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

Объявление OPTIMIZE содержит один или более списков, каждый из которых содержит один из символов: SPEED, SAFETY, SPACE, DEBUG или COMPILATION-SPEED и число от нуля до трех включительно. Число указывает относительный вес, который компилятор должен дать соответствующему параметру, причем 3 означает наиболее важное направление, а 0 - что это не имеет значения вообще. Таким образом, чтобы заставить компилятор Common Lisp компилировать функцию add более-менее так же, как это бы делал компилятор языка C, вы можете переписать ее так:

(defun add (x y)
  (declare (optimize (speed 3) (safety 0)))
  (declare (fixnum x y))
  (the fixnum (+ x y))
)

Конечно, теперь LISP-версия функции страдает многими из слабостей C-версии: если переданные аргументы не FIXNUM-значения или если сложение вызывает переполнение, результаты будут математически некорректны или даже хуже. Также если кто-то вызовет функцию add с неправильным количеством параметров, будет мало хорошего. Таким образом, вам следует использовать объявления этого вида только после того, как ваша программа стала работать правильно, и вы должны добавлять их только в местах, где профилирование показывает необходимость этого. Если вы имеете достаточную производительность без этих объявлений, пропускайте их. Но если профайлер показывает вам какое-то проблемное место в вашем коде и вам нужно оптимизировать его - вперёд. Поскольку вы можете использовать объявления таким образом, редко когда нужно переписывать код на языке C только из соображений производительности. Для доступа к существующему коду на C используется FFI, но когда нужна производительность, подобная языку C, используют объявления. И конечно, то, как близко вы захотите приблизить производительность данной части кода на языке Common Lisp к коду на C или C++, зависит главным образом от вашего желания.

Другое средство оптимизации, встроенное в язык Lisp, – это функция DISASSEMBLE. Точное поведение этой функции зависит от реализации, потому что оно зависит от того, как реализация компилирует код: в машинный код, байт-код или какую-то другую форму. Но основная идея в том, что она показывает вам код, сгенерированный компилятором, когда он компилировал данную функцию.

Таким образом, вы можете использовать DISASSEMBLE, чтобы увидеть, возымели ли ваши объявления какой-то эффект на генерируемый код. Если ваша реализация языка LISP использует компиляцию в машинный код, и если вы знаете язык ассемблера вашей платформы, вы сможете достаточно хорошо представить себе, что реально происходит, когда вы вызываете одну из ваших функций. Например, вы можете использовать DISASSEMBLE, чтобы понять, в чем различия между нашей первой версией add и окончательной версией. Сначала определите и скомпилируйте исходную версию

(defun add (x y) (+ x y))

Затем, в сессии REPL вызовите DISASSEMBLE с именем этой функции. В Allegro это выведет следующий ассемблероподобный листинг сгенерированного компилятором кода:

CL-USER> (disassemble 'add)
;; disassembly of #<Function ADD>
;; formals: X Y

;; code start: #x737496f4:
  0: 55         pushl        ebp
  1: 8b ec    movl        ebp,esp
  3: 56         pushl        esi
  4: 83 ec 24 subl        esp,$36
  7: 83 f9 02 cmpl        ecx,$2
 10: 74 02    jz        14
 12: cd 61    int        $97   ; SYS::TRAP-ARGERR
 14: 80 7f cb 00 cmpb        [edi-53],$0        ; SYS::C_INTERRUPT-PENDING
 18: 74 02    jz        22
 20: cd 64    int        $100  ; SYS::TRAP-SIGNAL-HIT
 22: 8b d8    movl        ebx,eax
 24: 0b da    orl        ebx,edx
 26: f6 c3 03 testb        bl,$3
 29: 75 0e    jnz        45
 31: 8b d8    movl        ebx,eax
 33: 03 da    addl        ebx,edx
 35: 70 08    jo        45
 37: 8b c3    movl        eax,ebx
 39: f8         clc
 40: c9         leave
 41: 8b 75 fc movl        esi,[ebp-4]
 44: c3         ret
 45: 8b 5f 8f movl        ebx,[edi-113]    ; EXCL::+_2OP
 48: ff 57 27 call        *[edi+39]   ; SYS::TRAMP-TWO
 51: eb f3    jmp        40
 53: 90         nop
; No value

Очевидно, что здесь полно всякой всячины. Если вы знакомы с ассемблером процессоров архитектуры x86, вы может быть это поймете. Теперь скомпилируйте версию функции add со всеми объявлениями:

(defun add (x y)
  (declare (optimize (speed 3) (safety 0)))
  (declare (fixnum x y))
  (the fixnum (+ x y))
)

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

CL-USER> (disassemble 'add)
;; disassembly of #<Function ADD>
;; formals: X Y

;; code start: #x7374dc34:
  0: 03 c2    addl        eax,edx
  2: f8         clc
  3: 8b 75 fc movl        esi,[ebp-4]
  6: c3         ret
  7: 90         nop
; No value

Похоже, что был.

Поставка приложений

Другая практически важная тема, о которой я не говорил нигде в этой книге, – это как поставлять приложения, написанные на языке Lisp. Главная причина, по которой я пренебрегал этой темой, заключается в том, что есть много разных способов это делать, и который из них лучше вам подходит, зависит от того, какой вид программного обеспечения вам нужно распространять, кому его надо распространять, и какой реализацией Common Lisp этот кто-то пользуется. В этом разделе я дам обзор некоторых из возможностей.

Если вы написали код, которым хотите поделиться со своими коллегами, которые тоже пишут на языке LISP, самый простой путь распространения – это распространять исходный код6). Вы можете поставлять простую библиотеку как один исходный файл, который программисты могут загрузить в их образ LISP-машины командой LOAD, возможно, после компиляции с помощью COMPILE-FILE.

Более сложные библиотеки или приложения, разбитые на несколько исходных файлов, ставят дополнительный вопрос: для того, чтобы загрузить сложный код, файлы исходного кода должны быть загружены и откомпилированы в правильном порядке. Например, файл, содержащий определения макросов, должен быть загружен до файлов, которые используют эти макросы, а файл, содержащий инструкцию определения пакета DEFPACKAGE должен быть загружен до того, как все файлы, использующие этот пакет, будут просто читаться READ-ом. Программисты на языке Lisp называют это проблемой определения систем (system definition problem) и обычно разрешают ее с помощью средств, называемых средствами определения систем (system definition facilities) или утилитами определения систем (system definition utilities), которые являются чем-то вроде аналогов билд-системам, таким, как утилиты make и ant. Как и эти средства, средства определения систем позволяют указать зависимости между разными файлами и берут на себя заботу о загрузке и компиляции этих файлов в правильном порядке, стараясь при этом выполнять только ту работу, которая необходима – например, перекомпилировать только те файлы, которые изменились.

В наши дни наиболее широко используется система определения систем, называемая ASDF, что означает Another System Definition Facility (Еще одно средство определения систем)7). Основная идея ASDF в том, что вы описываете вашу систему в специальном файле - ASD, а ASDF предоставляет возможности по выполнению некоторого набора операций с описанными таким образом системами, такие, как загрузка и компиляция систем. Система может быть объявлена зависимой от других систем, которые в таком случае будут корректно загружены при необходимости. Например, вот как выглядит содержимое файла "html.asd", содержащего описание системы для ASDF для библиотеки FOO из глав 31 и 32:

(defpackage :com.gigamonkeys.html-system (:use :asdf :cl))
(in-package :com.gigamonkeys.html-system)

(defsystem html
  :name "html"
  :author "Peter Seibel <peter@gigamonkeys.com>"
  :version "0.1"
  :maintainer "Peter Seibel <peter@gigamonkeys.com>"
  :license "BSD"
  :description "HTML and CSS generation from sexps."
  :long-description ""
  :components
  ((:file "packages")
   (:file "html" :depends-on ("packages"))
   (:file "css" :depends-on ("packages" "html"))
)

  :depends-on (:macro-utilities)
)

Если вы добавите символьную ссылку на этот файл в каталог, указанный в переменной asdf:*central-registry*8), то затем вы можете набрать следующую комманду

(asdf:operate 'asdf:load-op :html)

чтобы скомпилировать и загрузить файлы "packages.lisp", "html.lisp" и "html-macros.lisp" в правильном порядке после того, как система :macro-utilities будет скомпилирована и загружена. Примеры других файлов определения систем ASDF вы можете найти в прилагаемом к книге коде – код из каждой практической главы определён как система с соответствующими межсистемными зависимостями, определенными в формате ASDF.

Большинство свободного ПО и ПО с открытым кодом на языке Common Lisp, которое вы найдёте, будет поставляться с ASD файлом. Некоторые библиотеки могут поставляться с другими системами определения, такими, как немного более старая MK:DEFSYSTEM или даже системами, изобретенными авторами этой библиотеки, но общая тенденция, кажется все же, в использовании ASDF9).

Конечно, ASDF позволяет программистам легко устанавливать библиотеки, но это никак не помогает поставлять приложения конечным пользователям, которые не имеют представления о языке LISP. Если вы поставляете приложения для конечного пользователя, по-видимому, вы бы хотели предоставить пользователям нечто, что он бы мог загрузить, установить и запустить, не зная ничего о языке Lisp. Вы не можете ожидать, что пользователи будут отдельно устанавливать реализацию Lisp-а и вы бы наверное хотели, чтобы приложения, написанные на языке Lisp запускались так же, как и все прочие приложения в вашей операционной системе – двойным нажатием кнопки мыши на иконке приложения или набором имени приложения в командной строке интерпретатора команд.

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

И даже все еще более сложно – понятие "программа" не очень хорошо определено в языке LISP. Как вы видели во время чтения этой книги, процесс разработки программ на языке LISP – это инкрементальный процес, подразумевающий постоянные изменения определений и данных, находящихся внутри исполняемого образа LISP-машины. Поэтому "программа" – это всего лишь определенное состояние этого образа, которое достигнуто в результате загрузки файлов .lisp или .fasl, которые содержат код, который создает соответствующие определения и данные. Вы могли бы соответственно распространять приложение на языке LISP как рантайм LISP-машины, плюс набор файлов .fasl и исполняемый модуль, который запускал бы рантайм LISP-машины, загружал бы все файлы .fasl и как-то вызывал бы соответствующую начальную функцию вашего приложения. Однако, поскольку в реальности загрузка файлов .fasl может занимать значительное время, особенно если они должны выполнить какие-то вычисления для установки начального состояния данных, большинство реализаций Common LISP предоставляют способ выгрузить исполняемый образ LISP-машины в файл, называемый файл образа (image file) или иногда core file, чтобы сохранить все состояние LISP-машины. Когда рантайм LISP-машины запускается, первым делом он загружает файл образа, что у него получается гораздо быстрее, чем если бы то же состояние восстанавливалось с помощью загрузки файлов .fasl.

Обычно файл образа – это базовый образ LISP-машины, содержащий только стандартные пакеты, определенные стандартом языка и какие-то дополнительные пакеты, предоставляемые данной реализацией. Но в большинстве реализаций вы можете указать другой, свой файл образа. Таким образом, вместо поставки приложения в виде рантайма LISP-машины и набора файлов .fasl, вы можете поставлять рантайм LISP-машины и только один файл образа, содержащий все определения кода и данных, которые составляют ваше приложение. В таком случае вам будет надо только запустить рантайм с этим файлом образа и вызвать функцию, служащую точкой входа в ваше приложение.

Здесь всё становится зависящим от реализации и операционной системы. Некоторые реализации Common Lisp, в особенности коммерческие, как например Allegro или LispWorks, предоставляют средства для создания таких файлов образа. Например, Allegro Enterprise Edition предоставляет функцию excl:generate-application, которая создаёт каталог, содержащий рантайм языка Lisp как разделяемую библиотеку, файл образа, и выполняемый файл, который запускает рантайм с данным образом. Похожим образом механизм LispWorks Professional Edition, называемый "поставка" (delivery), позволяет вам строить однофайловый исполняемый файл из ваших программ. В различных реализациях Unix вы можете делать фактически то же самое, хотя возможно там проще использовать командный файл для запуска всего, что угодно.

В Mac OS X всё ещё лучше: поскольку все приложения в этой ОС упакованы в .app-пакеты, которые по сути являются каталогами с определённой структурой, очень легко упаковать все части приложения на LISP в .app-пакет, который можно запустить двойным нажатием клавиши мыши. Утилита Bosco Микеля Эвинса (Mikel Evins) делает создание .app-пакетов простым для приложений, работающих на OpenMCL.

Конечно, в наши дни другой популярный способ поставки приложений – это серверные приложения. Это – та ниша, где Common Lisp может превосходно использоваться: вы можете выбрать сочетание операционной системы и реализации Common Lisp, которая вас устраивает, и вам не нужно заботиться о поставке приложения в том виде, в котором его мог бы установить пользователь. А возможность языка Common Lisp интерактивно разрабатывать и отлаживать программы позволяет вам отлаживаться и обновлять версии вашей программы без остановки сервера, что либо вообще было бы невозможно в менее динамичных языках программирования, либо требовало бы построения довольно сложной инфраструктуры поддержки.

Что дальше

Ну вот и все. Добро пожаловать в чудесный мир языка LISP. Лучшее, что вы можете сейчас сделать (если вы уже это не сделали) – это начать писать свои собственные программы на языке LISP. Выберите проект, который вам интересен, и сделайте его в Common LISP. Потом сделайте еще один. Как говориться, намылить, прополоскать, повторить ...

Однако, если вам нужна дальнейшая помощь, этот раздел даст вам несколько мест, где ее можно найти. Для начинающих полезно посмотреть сайт этой книги "Practical Common Lisp Web site" по ссылке http://www.gigamonkeys.com/book, где вы найдете исходный код из практических глав книги, список опечаток, и ссылки на другие ресурсы в сети Интернет.

В добавок к сайтам, которые я упомянул в разделе "Поиск библиотек LISP", вы возможно заходите изучить Common Lisp HyperSpec (известную также как HyperSpec или CLHS), которая является HTML-версией ANSI-стандарта языка, приготовленой Кентом Питманом (Kent Pitman) и сделанной общедоступной компанией LispWorks по ссылке http://www.lispworks.com/documentation/HyperSpec/index.html. HyperSpec ни в коем случае не является учебником, но это – самое надёжное руководство по языку, которое вы только можете достать, не покупая печатную версию стандарта языка у комитета ANSI, и при том гораздо более удобное в повседневном использовании.10)

Если вы хотите связаться с другими лисперами, Usenet-группа (группа новостей) "comp.lang.lisp" и IRC-канал "#lisp" в чат-сети Freenode (http://www.freenode.net) – вот два основных сборища лисперов в сети11). Существует также некоторе количество блогов, связанных с языком LISP, большее количество которых собрано на сайте "Planet Lisp" ("Планета ЛИСП") по адресу http://planet.lisp.org.

И не пропускайте также во всех этих форумах анонсы собраний местных групп пользователей языка ЛИСП – в последние несколько лет собрания лисперов возникают спонтанно во всех городах мира, от Нью-Йорка до Окленда, от Кёльна до Мюнхена, от Женевы до Хельсинки.

Если же вы хотите продолжить изучение книг, вот вам несколько советов. В качестве хорошего толстого справочника можете держать на вашем столе книгу "The ANSI Common Lisp Reference Book" под редакцией Дэвида Марголиса (David Margolies)(издательство Apress, 2005 г.)12).

Для детального изучения объектной системы языка LISP вы можете для начала прочитать книгу Сони Кин (Sonya E. Keene) "Object-Oriented Programming in Common Lisp: A Programmer's Guide to CLOS" ("Объектно-ориентированное программирование в Common LISP: Руководство программиста по CLOS.") (издательство Addison-Wesley, 1989 г.). Затем, если вы хотите стать настоящим мастером, или просто чтобы расширить кругозор, прочитайте книгу авторов Грегора Кикзалеса, Джима де Ривьереса и Даниэля Боброва (Gregor Kiczales, Jim des Rivieres, Daniel G. Bobrow) "The Art of the Metaobject Protocol" (издательство MIT Press, 1991 г). Эта книга, также известная как AMOP, является как объяснением, что такое метаобъектный протокол и зачем он нужен, так и фактически стандартом метаобъектного протокола, поддерживаемого многими реализациями языка Common Lisp.

Две книги, которые охватывают общие приёмы программирования на Common LISP, – это "Paradigms of Artificial Intelligence Programming: Case Studies in Common Lisp" Питера Норвига (Peter Norvig) (издательство Morgan Kaufmann, 1992 г) и "On Lisp: Advanced Techniques for Common Lisp" Пола Грема (Paul Graham) (издательство Prentice Hall, 1994 г). Первая даёт твёрдую основу приёмов искусственного интеллекта, и в то же время учит немного, как писать хороший код на Common Lisp. Вторая особенно хороша в рассмотрении макросов.

Если вы – человек, который любит знать, как всё работает до последнего винтика, книга Кристиана Квене (Christian Queinnec) "Lisp in Small Pieces" (издательство Cambridge University Press, 1996 г) сможет дать вам удачное сочетание теории языков программирования и практических методик реализации языка LISP. Несмотря на то, что она изначально сконцентрирована на реализации языка Scheme, а не Common Lisp, принципы, изложенные в книге, применимы и для него.

Для людей, которые хотят более теоретического взгляда на вещи, или для тех, кто просто хочет попробовать, каково это – быть новичком-студентом компьютерных наук в Массачусетсском технологическом институте – следующая книга авторов Харольда Абельсона, Геральда Джея Сусмана и Джули Сусман (Harold Abelson, Gerald Jay Sussman, and Julie Sussman) – "Structure and Interpretation of Computer Programs, Second Edition" ("Структура и интерпретация компьютерных программ") (издательство M.I.T. Press, 1996 г), классическая работа по компьютерным наукам, которая использует язык Scheme для обучения важным концепциям программирования. Любой программист может много узнать из этой книги, не забывайте только, что у языков Scheme и Common Lisp есть важные отличия.

Как только вы охватите своим умом язык Lisp, вам возможно захочется добавить чего-то объектного. Поскольку никто не может заявлять, что он действительно понимает объекты без знания хотя-бы чего-то о языке Smalltalk, вы, возможно, захотите начать знакомство с этим языком с книги Адели Голдберг и Дэвида Робсона (Adele Goldberg, David Robson) "Smalltalk-80: The Language" (издательство Addison Wesley, 1989 г), которая является стандартным введением в язык Smalltalk. После этого – книга Кента Бека (Kent Beck) "Smalltalk Best Practice Patterns" (издательство Prentice Hall, 1997), которая полна хороших советов для программистов на этом языке, большинство из которых также применимо и к любому другому объектно-ориентированному языку.

На другом конце спектра – книга Бертрана Мейера (Bertrand Meyer), изобретателя часто незамечаемого наследника Симулы и Алгола – языка Eiffel, "Object-Oriented Software Construction" (Prentice Hall, 1997), прекрасная демонстрация склада ума людей, мыслящих в стиле статических языков программирования. Эта книга содержит много пищи для размышлений даже для программистов, работающих с динамическими языками, каковым является язык Common Lisp. В частности, идеи Мейера о контрактном программировании могут помочь понять, как нужно использовать систему условий языка Common Lisp.

Хотя и не о компьютерах как таковых, книга Джеймса Суровьеки "Мудрость масс: почему те, кого много, умнее тех, кого мало, и как коллективная мудрость формирует бизнес, экономику, общество и нации". ("The Wisdom of Crowds: Why the Many Are Smarter Than the Few and How Collective Wisdom Shapes Business, Economies, Societies, and Nations", James Surowiecki, Doubleday, 2004) содержит великолепный ответ на вопрос: "почему если LISP такой замечательный, его все не используют?" Смотрите раздел "Лихорадка досчатой дороги", начиная со страницы 53.

И в заключении для удовольствия и чтобы понять, какое влияние LISP и лисперы оказали на развитие культуры хакеров, просмотрите или прочитайте от корки до корки "Новый словарь хакеров" Эрика реймонда ("The New Hacker's Dictionary, Third Edition, compiled by Eric S. Raymond, MIT Press, 1996) которое базируется на исходном словаре хакеров, редактируемом Гаем Стилом (Harper & Row, 1983).

Но не давайте всем этим книгам мешать вашему программированию, потому что единственный путь изучить язык программирования – это использовать его. Если вы дошли до этого места, вы безусловно в состоянии заняться этим. Счастливо покодить!

1)Сочетание средств выборочного чтения исходного кода (#+, #-) и макросов дает возможность разрабатывать для нестандартных средств слои переносимости, которые ничего не делают, кроме предоставления общего API, развёрнутого поверх специфичного API конкретной реализации. Переносимая библиотека файловых путей из главы 15 – пример библиотеки такого рода, хотя она скорее служит не для устранения различий в API разных реализаций, а для сглаживания различий в интерпретации стандарта языка авторами разных реализаций.
2)Foreign Function Interface эквивалентен в основном JNI в языке Java, XS в языке Perl, или модулю расширения языка Python.
3)Два главных недостатка библиотеки UFFI – это отсутствие поддержки вызова LISP-функций из кода на языке C, которую предоставляют многие, но не все FFI реализаций, и отсутствие поддержки CLISP, - одной из популярных реализаций Common Lisp - библиотека FFI которой хотя и достаточно хороша, но сильно отличается от FFI других реализаций, и поэтому не вписывается легко в рамки модели UFFI.
4)Кнут использовал эту фразу несколько раз в своих публикациях, включая его работу 1974-го года "Программирование как искусство" ("Computer Programming as an Art"), удостоенную премии Тьюринга, и в его работе "Структурное программирование с использованием оператора goto" ("Structured Programs with goto Statements."). В его работе "Ошибки в TeX" ("The Errors of TeX") он приписывал эту фразу Чарльзу Хоару. А Хоар в своем электронном письме от 2004 года к Гансу Генвицу из компании phobia.com (Hans Genwitz of phobia.com) признался, что не помнит точно происхождения этой фразы, но он бы приписал ее авторство Дейкстре.
5)Объявления могут появляться в большинстве форм, которые вводят новые переменные, как например LET, LET* и семейство описывающих циклы макросов DO. LOOP имеет свой механизм объявления типов переменных цикла. Специальный оператор LOCALLY, упоминаемый в главе 20, делает ни что иное, как создает место для объявлений.
6)Файлы FASL, получающиеся после компиляции с помощью COMPILE-FILE, имеют формат, зависящий от реализации и могут даже быть несовместимы с разными версиями одной и той же реализации. Таким образом, они – не очень хороший способ распространения кода на языке Lisp. В одном случае они могут быть удобны – как способ обеспечения исправлений вашего приложения, которое работает на какой-то определенной версии конкретной реализации. Тогда чтобы исправить ваше приложение достаточно загрузить FASL-файл с помощью LOAD, и, поскольку FASL-файл может содержать любой код, он также может быть использован как для определения новой версии кода, так и для изменения существующих данных, чтобы они соответствовали новой версии
7)ASDF был написан Дэниэлем Барлоу (Daniel Barlow), одним из разработчиков SBCL, и был включен в SBCL как часть, а также поставлялся как самостоятельная библиотека. В последнее время он был заимствован и включен в другие реализации LISP, такие, как OpenMCL и Allegro.
8)В системе Windows, где нет символьных ссылок, это надо делать немного по-другому, но принцип тот же. (Примечание переводчика: На самом деле в современном Win32 API поддерживаются символьные ссылки в файловой системе NTFS, правда операции с ними недоступны в большинстве стандартного ПО для работы с файлами, но предоставляются такой известной программой, как Far. Во времена создания этой книги данная возможность уже существовала, но, видимо, автор не был с нею знаком ввиду малораспространенности средств, поддерживающих ее. Кроме того, символьные ссылки в Unix/Lunux-образных операционных системах имеют некоторые особенности, в результате которых пути к файлам относительно символьной ссылки вычисляются с использованием того места, куда эта ссылка ссылается. В Win32 такого не происходит, поэтому этот способ для аналогичных целей просто неприменим в Win32.)
9)Другое средство, ASDF-INSTALL, построенное поверх систем ASDF и MK:DEFSYSTEM, предоставляет простой способ автоматически скачать библиотеки из Интернета и загрузить их. Лучший способ начать знакомство с ASDF-INSTALL – инструкция, написанная Эди Вайтзом (Edi Weitz) "A tutorial for ASDF-INSTALL" http:// www.weitz.de/asdf-install/.
10)SLIME включает в свой состав библиотеку на языке Elisp (Emacs lisp), которая позволяет вам автоматически попадать по ключевому слову, определённому в стандарте, на статью в HyperSpec. Вы также можете загрузить полную копию HyperSpec из Интернета и использовать её полностью локально.
11)Примечание переводчика: Здесь сложно было бы удержаться от упоминания русскоязычного канала lisp@conference.jabber.ru, благодаря существованию которого данная книга и смогла появиться на свет на русском языке.
12)Другой классический справочник – книга Гая Стила, (Guy Steele) "Common Lisp: The Language" (издательство Digital Press, 1984 и 1990 гг). Первое издание, известное также как CLtL1, было фактически стандартом языка несколько лет. Ожидая, пока официальный стандарт ANSI будет закончен, Гай Стил, который также был в комитете ANSI по языку LISP, решил выпустить второе издание, чтобы восполнить разрыв между CLtL1 и окончательным стандартом. Второе издание, теперь известное также как CLtL2, по сути слепок работы комитера по стандартизации, снятый в определённым момент времени, близкий к концу, но всё-таки не в самом конце процесса стандартизации. Следовательно, CLtL2 отличается от стандарта в некоторых аспектах, что делает эту книгу не очень хорошим справочником. Однако она всё же является полезным историческим документом, особенно потому, что она включает документацию по некоторым возможностям, которые были исключены из окончательного стандарта, а также не входящие в стандарт комментарии о том, почему что-то сделано именно так.
Предыдущая Оглавление
@2009-2013 lisper.ru