Permalink
Switch branches/tags
Nothing to show
Find file
Fetching contributors…
Cannot retrieve contributors at this time
786 lines (660 sloc) 69.8 KB
\chapter{Программирование по-взрослому: пакеты и символы}
\label{ch:21}
\thispagestyle{empty}
В~главе \ref{ch:04} я рассказывал, как процедура чтения Lisp переводит текстовые имена в
объекты, которые затем передаются вычислителю в виде так называемых \textit{символов}.
Оказывается, что иметь встроенный тип данных специально для представления имён очень
удобно для многих видов программирования\pclfootnote{Способ программирования, основывающийся
на типе данных \textit{символ}, называется, вполне подходяще, \textit{символьной}
обработкой. Обычно он противопоставляется \textit{численному} программированию. Пример
программы, с которой должны быть хорошо знакомы все программисты и которая занимается
почти исключительно символьными преобразованиями,~--- компилятор. Он принимает текст
программы как набор символов и преобразует его в новую форму.}. Это, однако, не тема
данной главы. В~этой главе я расскажу об одном из наиболее явных и практических аспектов
работы с именами: как избегать конфликта между независимо разрабатываемыми кусками кода.
Предположим, для примера, что вы пишете программу и решаете использовать чью-то
библиотеку. Вы не хотите знать имена всех функций, переменных, классов или макросов
внутри этой библиотеки, чтобы избежать путаницы между её именами и именами, которые вы
используете в своей программе. Вы бы хотели, чтобы большинство имён в библиотеке и имён в
вашей программе рассматривалось отдельно, даже если они окажутся одинаковыми в написании.
В~то же самое время вы хотите, чтобы некоторые имена, определённые в библиотеке, были
легко доступны~--- те имена, что составляют её публичный API, который вы как раз хотите
использовать.
В Common Lisp эта проблема пространства имён сводится просто к вопросу конт\-ро\-ля за тем,
как процедура чтения переводит текстовые имена в символы: если вы хотите, чтобы два появления
одного и того же имени рассматривались интерпретатором одинаково, вы должны убедиться, что
процедура чтения использует один и тот же символ для представления каждого из них. И наоборот,
если нужно, чтобы два имени рассматривались как разные, даже если они совпадают
побуквенно, вам надо, чтобы процедура чтения создала разные символы для представления этих имён.
\section{Как процедура чтения использует пакеты}
В~главе~\ref{ch:04} я коротко рассказал, как процедура чтения в Lisp переводит имена в символы,
но я пропустил множество деталей~--- теперь настало время поближе взглянуть на то, что же
происходит на самом деле.
Начну с описания синтаксиса имён, понимаемых процедурой чтения, и того, как он соотносится с
пакетами. На данный момент можете представить себе пакеты как таблицы, которые
отображают строки в символы. Как вы увидите в дальнейшем, в действительности это
отображение можно регулировать более тонко, чем в простой таблице соответствий, но не теми
способами, которые использует процедура чтения. Каждый пакет также имеет имя, которое может
быть использовано для поиска пакета через функцию \lstinline{FIND-PACKAGE}.
Две ключевые функции, которые процедура чтения использует для доступа к отображению
имя-в-символ в пакете,~--- это \lstinline{FIND-SYMBOL} и \lstinline{INTERN}. Обе они получают строку и,
необязательно, пакет. Если пакет не указывается, на его место подставляется значение
глобальной переменной \lstinline{*PACKAGE*}, называемой также \textit{текущим пакетом}.
\lstinline{FIND-SYMBOL} ищет в пакете символ с именем, совпадающим со строкой, и возвращает его
или \lstinline{NIL}, если символ не найден. \lstinline{INTERN} также возвратит существующий символ,
но если его нет, создаст новый, назначит строку его именем и поместит в пакет.
Большинство имён, используемых вами,~--- неспециализированные имена, не содержащие
двоеточий. Когда процедура чтения читает такое имя, он переводит его в символ путём изменения
всех неэкранированных букв в верхний регистр и передавая полученную строку \lstinline{INTERN}.
Таким образом каждый раз, когда процедура чтения читает то же имя в том же пакете, он получает
тот же объект-символ. Это важно, так как интерпретатор использует объектное совпадение
символов, чтобы определить, к какой функции, переменной или другому программному элементу
данный символ относится. То есть причина, по которой выражение вида \lstinline{(hello-world)}
преобразуется в вызов определённой hello-world фукции,~--- это возврат процедурой чтения одного и
того же символа, и когда он читает вызов функции, и когда он он читал форму \lstinline{DEFUN},
которая эту функцию определяла. Имя, содержащее двоеточие или двойное двоеточие, является
пакетно-специализированным именем. Когда процедура чтения читает пакетно-специализированное
имя, он разбивает его в месте двоеточия(й) и берёт первую часть как имя пакета, а вторую
как имя символа. Затем процедура чтения просматривает соответствующий пакет и использует его
для перевода имени в символ-объект.
Имена, содержащие только одинарное двоеточие, относятся к внешним символам, тем, которые
пакет экспортирует для общего пользования. Если названный пакет не содержит символа с
данным именем, или содержит, но он не экспортирован, процедура чтения сигнализирует об
ошибке. Имена с двойным двоеточием могут обозначать любой символ в названном пакете, хотя
это обычно плохой тон~--- множество экспортированных символов определяют публичный
интерфейс пакета, и если вы не уважаете решение автора пакета о том, какие имена делать
внешними, а какие внутренними, вы напрашиваетесь на неприятности в дальнейшем. С другой
стороны, иногда авторы пакетов забывают экспортировать символы, явно предназначенные для
внешнего пользователя. В~таком случае имена с двойным двоеточием позволят вам работать, не
дожидаясь выпуска следующей версии пакета.
Ещё два аспекта в синтаксисе символов, которые понимает процедура чтения,~--- это ключевые и
внепакетные символы. Ключевые символы записываются как имена, начинающиеся с
двоеточия. Такие символы добавляются в пакет, названный \lstinline{KEYWORD}, и экспортируются
автоматически. Кроме того, когда процедура чтения добавляет символ в \lstinline{KEYWORD}, он также
определяет константную переменную с символом в качестве как имени, так и значения. Вот
почему вы можете использовать ключевые слова в списке аргументов без кавычки спереди~---
когда вычисляется их значение, оно оказывается равным им самим. Таким образом:
\begin{myverb}
(eql ':foo :foo) ==> T
\end{myverb}
Имена ключевых символов, как и всех остальных, перед интернированием преобразуются
процедурой чтения к верхнему регистру. В~имя не включается двоеточие.
\begin{myverb}
(symbol-name :foo) ==> "FOO"
\end{myverb}
Внепакетные символы записываются с поставленными впереди \lstinline!#:!. Эти имена (без
\lstinline!#:!) преобразуются в верхний регистр, как обычно, и затем переводятся в
символы, но эти символы не добавляются ни в один пакет; каждый раз, когда процедура чтения
читает имя с \lstinline!#:!, он создаёт новый символ. Таким образом:
\begin{myverb}
(eql '#:foo '#:foo) ==> NIL
\end{myverb}
Вы очень редко будете пользоваться в записи таким синтаксисом, если вообще будете, но
иногда вы будете видеть его при печати s-выражения с символом, возвращённым функцией
\lstinline{GENSYM}.
\begin{myverb}
(gensym) ==> #:G3128
\end{myverb}
\section{Немного про словарь пакетов и символов}
Как я упомянул ранее, соответствие между именами и символами, предоставленными пакетом,
устроено более гибко, чем простая таблица соответствий. Ядром каждого пакета является
поисковая таблица имя-в-символ, но символ в пакете можно сделать доступным через
неспециализированное имя и другим путём. Чтобы поговорить об этих иных механизмах, вам
понадобятся некоторые термины.
Для начала все символы, которые могут быть найдены в данном пакете с по\-мощью функции
\lstinline{FIND-SYMBOL}, называются \textit{доступными} в этом пакете. Иными словами,
доступными символами в пакете являются те, на которые можно ссылаться
неспециализированными именами, когда пакет является текущим.
Символ может быть доступен в пакете двумя способами. Первый, когда пакетная имя-в-символ
таблица содержит запись для этого символа. В~этом случае мы говорим, что символ
\textit{присутствует} в пакете. Когда процедура чтения интернирует новый символ в пакет, он
добавляет его в таблицу имя-в-символ. Первый пакет, в который интернирован символ,
называется \textit{домашним пакетом} этого символа.
Другой способ, которым символ может быть доступен в пакете,~--- это когда пакет
\textit{наследует} его. Пакет наследует символы из других пакетов путём
\textit{использования} их. Наследуются только внешние символы из используемых
пакетов. Символ делается внешним в пакете путём его \textit{экспорта}. В~дополнение к
тому, что он наследуется ис\-поль\-зую\-щим пакетом, экспорт символа~--- как вы видели в
предыдущих разделах~--- делает возможным ссылаться на символ через специализированное имя с
двоеточием.
Чтобы сохранить отображение имён в символы однозначным, пакетная система позволяет только
одному символу быть доступным в данном пакете для каждого имени. То есть в пакете не
могут присутствовать символ и наследованный символ с одним и тем же именем или два разных
наследованных символа из двух разных пакетов с одинаковым именем. Однако вы можете
разрешить конфликт, делая один из доступных символов \textit{скрывающим} символом, что
делает другой символ с таким же именем недоступным. В~дополнение к своей таблице
имя-в-символ каждый пакет содержит список скрывающих символов.
Существующий символ может быть \textit{импортирован} в другой пакет через добавление его в
пакетную таблицу имя-в-символ. Таким образом, один и тот же символ может присутствовать
во многих пакетах. Иногда вы будете импортировать символы просто потому, что вы хотите их
сделать доступными в импортирующем пакете без использования их домашнего пакета. В~другой
раз вы экспортируете символ потому, что только присутствующий символ может быть
экспортирован или быть скрывающим символом. Например, если пакету надо использовать два
пакета, которые содержат внешние символы с одинаковым именем, один из символов должен быть
импортирован в ис\-поль\-зую\-щий пакет, чтобы быть добавленным в список скрывающих и сделать
другой символ недоступным.
Наконец, присутствующий символ может быть сделан \textit{внепакетным}, что ведёт к его
удалению из таблицы имя-в-символ и, если это был скрывающий символ, из списка скрывающих.
Вы можете выкинуть символ из пакета для разрешения конфликта между символом и внешним
символом пакета, который вы хотите использовать. Символ, который не присутствует ни в
одном из пакетов, названный \textit{внепакетным} символом, не может быть более прочтён
процедурой чтения и будет печататься, используя \lstinline!#:foo! синтаксис.
\section{Три стандартных пакета}
В~следующем разделе я покажу вам, как создавать ваши собственные пакеты, включая создание
одного пакета с использованием другого, и как экспортировать, скрывать и импортировать
символы. Но вначале давайте посмотрим на несколько пакетов, которыми вы уже
пользовались. Когда вы запускаете Lisp, значением \lstinline{*PACKAGE*} обычно является пакет
\lstinline{COMMON-LISP-USER}, также известный как \lstinline{CL-USER}\footnote{У каждого пакета
есть одно официальное имя и ноль или больше псевдонимов, которые могут быть использованы
везде, где нужно имя пакета. То есть в таких случаях, как пакетно-специализированные
имена или ссылка на пакет в формах \lstinline{DEFPACKAGE} или \lstinline{IN-PACKAGE}.}\hspace{\footnotenegspace}.
\lstinline{CL-USER} использует пакет \lstinline{COMMON-LISP}, который экспортирует все имена из
языкового стандарта. Таким образом, когда вы набираете выражение в REPL, все имена
стандартных функций, макросов, переменных и т.~д. будут преобразованы в символы,
экспортированные из \lstinline{COMMON-LISP}, и все другие имена будут интернированы в пакет
\lstinline{COMMON-LISP-USER}. Например, имя \lstinline{*PACKAGE*} экспортировано из
\lstinline{COMMON-LISP}~--- если вы хотите увидеть значение \lstinline{*PACKAGE*}, наберите
следующее:
\begin{myverb}
CL-USER> *package*
#<The COMMON-LISP-USER package>
\end{myverb}
\noindent{}потому что \lstinline{COMMON-LISP-USER} использует \lstinline{COMMON-LISP}. Или вы можете задать
пакетно-специализированное имя.
\begin{myverb}
CL-USER> common-lisp:*package*
#<The COMMON-LISP-USER package>
\end{myverb}
Вы даже можете использовать \lstinline{CL}, псевдоним \lstinline{COMMON-LISP}.
\begin{myverb}
CL-USER> cl:*package*
#<The COMMON-LISP-USER package>
\end{myverb}
Однако \lstinline{*X*} не является символом из \lstinline{COMMON-LISP}, так что если вы наберёте:
\begin{myverb}
CL-USER> (defvar *x* 10)
*X*
\end{myverb}
\noindent{}процедура чтения прочтёт \lstinline{DEFVAR} как символ из пакета \lstinline{COMMON-LISP} и \lstinline{*X*}
как символ из \lstinline{COMMON-LISP-USER}.
REPL не может запускаться в пакете \lstinline{COMMON-LISP}, потому что вам не позволено вводить
никакие символы в него; \lstinline{COMMON-LISP-USER} работает как <<черновой>> пакет, в котором вы
можете создавать собственные имена, в то же время имея лёгкий доступ ко всем символам из
\lstinline{COMMON-LISP}\footnote{\lstinline{COMMON-LISP-USER} также позволено предоставлять доступ
к символам, экспортированными некоторыми другими, в зависимости от реализации
пакетами. Хотя это сделано для удобства пользователя~--- это делает специфическую для
каждого воплощения функциональность готовой к употреблению, однако также служит
причиной растерянности для новичков: Lisp будет возмущаться попыткой переопределить
некоторые имена, не входящие в стандарт языка. Посмотреть, из каких пакетов
\lstinline{COMMON-LISP-USER} наследует символы в данной реализации, можно, выполнив следующее
выражение в REPL:
\begin{myverb}
(mapcar #'package-name (package-use-list :cl-user))
\end{myverb}
А чтобы найти, из какого изначального пакета взят символ, выполните это:
\begin{myverb}
(package-name (symbol-package 'some-symbol))
\end{myverb}
\noindent{}где some-symbol надо заменить на запрашиваемый символ. Например:
\begin{myverb}
(package-name (symbol-package 'car)) ==> "COMMON-LISP"
(package-name (symbol-package 'foo)) ==> "COMMON-LISP-USER"
\end{myverb}
Символы, унаследованные от пакетов, определённых вашей реализацией, возвратят несколько
другие значения.}\hspace{\footnotenegspace}. Обычно все созданные вами пакеты будут так же использовать
\lstinline{COMMON-LISP}, так что вы не должны писать нечто вроде:
\begin{myverb}
(cl:defun (x) (cl:+ x 2))
\end{myverb}
Третий стандартный пакет~--- это пакет \lstinline{KEYWORD}, который процедура чтения Lisp
использует, чтобы хранить имена, начинающиеся с двоеточия. Таким образом, вы также можете
ссылаться на любой ключевой символ, с явным указанием пакета, как здесь:
\begin{myverb}
CL-USER> :a
:A
CL-USER> keyword:a
:A
CL-USER> (eql :a keyword:a)
T
\end{myverb}
\section{Определение собственных пакетов}
Работать в \lstinline{COMMON-LISP-USER} замечательно для экспериментов в REPL, но как только вы
начнёте писать настоящую программу, то вы захотите определить новый пакет, чтобы различные
программы, загруженные в одну среду Lisp, не топтались на именах друг друга. И когда вы
пишете библиотеку, которую намереваетесь использовать в различных контекстах, вы захотите
определить различные пакеты и затем экспортировать символы, которые составляют публичный
API библиотеки.
Однако перед тем, как вы начнёте определять пакет, важно понять одну вещь про то, чем
пакеты \textit{не занимаются}. Пакеты не предоставляют прямой контроль за тем, кто может
вызвать какую функцию и к какой переменной кому позволено иметь доступ. Они предоставляют
вам базовое управление пространством имён через контроль за тем, как процедура чтения
преобразует текстовые имена в символьные объекты, но не за тем, как потом в интерпретаторе
этот символ рассматривается как имя функции, переменной или чего-нибудь ещё. Таким
образом, бессмысленно говорить про экспорт функции или переменной из пакета. Вы можете
экспортировать символ, чтобы сделать определённое имя легкодоступным, но пакетная система
не позволяет вам ограничивать, как оно будет использовано\pclfootnote{Это отличается от
пакетной системы Java, которая предоставляет пространство имён для классов, но также
включает Java-механизм контроля доступа. Язык не из семейства Lisp с похожей на Common
Lisp пакетной системой~--- это Perl.}.
Запомнив это, вы можете начать рассмотрение того, как определять пакеты и увязывать их
друг с другом. Вы определяете новые пакеты через макрос \lstinline{DEFPACKAGE}, который даёт
возможность не только создать пакет, но и определить, какие пакеты он будет использовать,
какие символы экспортирует, какие символы импортирует из других пакетов, и разрешить
конфликты посредством скрытия символов\footnote{Все манипуляции, выполняемые через
\lstinline{DEFPACKAGE}, также могут быть выполнены функциями, которые манипулируют
пакетными объектами. Однако, так как пакет, вообще говоря, должен быть полностью
определён, перед тем как он может быть использован, эти функции редко находят
применение. Также \lstinline{DEFPACKAGE} заботится о выполнении всех манипуляций с пакетом
в правильном порядке, например \lstinline{DEFPACKAGE} добавляет символы в список
скрывающих перед тем, как он пытается использовать подключённые пакеты.}\hspace{\footnotenegspace}.
Я буду описывать различные опции в свете того, как вы можете использовать пакеты в
написании программы, организующей почтовые сообщения в поисковую базу данных. Программа
целиком гипотетическая, так же, как и библиотеки, на которые я буду ссылаться,~--- смысл в
том, чтобы взглянуть, как могут быть структурированы пакеты, используемые в такой
программе.
Первый пакет, который вам нужен,~--- это тот, который предоставляет пространство имён для
приложений~--- вы захотите именовать ваши функции, переменные и т.~д. без заботы о
коллизии имён с не относящимся к делу кодом. Так вы определите новый пакет посредством
\lstinline{DEFPACKAGE}.
Если приложение достаточно просто, чтобы обойтись без библиотек сверх средств,
предоставляемых самим языком, вы можете определить простой пакет примерно так:
\begin{myverb}
(defpackage :com.gigamonkeys.email-db
(:use :common-lisp))
\end{myverb}
Здесь определяется пакет, названный \lstinline{COM.GIGAMONKEYS.EMAIL-DB}, который наследует все
символы, экспортируемые пакетом \lstinline{COMMON-LISP}\footnote{Во многих реализациях Lisp
пункт \lstinline{:use} необязателен, если вы хотите просто \lstinline{:use} (использовать)
\lstinline{COMMON-LISP}: если он пропущен, пакет автоматически наследует имена от всего
определённого для данного воплощения списка пакетов, который обычно включает и
\lstinline{COMMON-LISP}. Однако ваш код будет чуть более портируем, если вы всегда будете
явно указывать все пакеты, которые хотите использовать (\lstinline{:use}). Те, кому
не хочется много печатать, могут задействовать псевдонимы и написать \lstinline{(:use :cl)}.}\hspace{\footnotenegspace}.
У вас на самом деле есть выбор, как представлять имена пакетов и, как вы увидите, имена
символов в \lstinline{DEFPACKAGE}. Пакеты и символы называются с помощью строк. Однако в форме
\lstinline{DEFPACKAGE} вы можете задать имена пакетов и символов через \textit{строковые
обозначения}. Строковыми обозначениями являются строки, которые обозначают сами себя;
символы, которые обозначают свои имена; или знак, который означает однобуквенную строку,
содержащую только этот знак. Использование ключевых символов, как в вышеприведённом
\lstinline{DEFPACKAGE}, является общепризнанным стилем, который позволяет вам писать имена в
нижнем регистре~--- процедура чтения преобразует для вас имена в верхний регистр. Также можно
записывать \lstinline{DEFPACKAGE} с помощью строк, но тогда вы должны писать их все в верхнем
регистре, потому что настоящие имена большинства символов и пакетов фактически в верхнем
регистре из-за соглашения о преобразовании, которое выполняет
процедура чтения\footnote{Использование ключевых слов вместо строк также имеет другое
преимущество~--- Allegro предоставляет <<современный стиль>> Lisp, в котором процедура чтения
не совершает преобразования имён и в котором вместо пакета \lstinline{COMMON-LISP} с
именами в верхнем регистре предоставляется пакет \lstinline{common-lisp} с именами в нижнем.
Строго говоря, такой Lisp не удовлетворяет требованиям Common Lisp, так как все имена по
стандарту определены в верхнем регистре. Однако, если запишете свою форму
\lstinline{DEFPACKAGE}, используя ключевые символы, она будет работать как в Common Lisp, так
и в его ближайших родственниках.}\hspace{\footnotenegspace}.
\begin{myverb}
(defpackage "COM.GIGAMONKEYS.EMAIL-DB"
(:use "COMMON-LISP"))
\end{myverb}
Вы могли бы также использовать неключевые символы~--- имена в \lstinline{DEFPACKAGE} не
интерпретируются, но тогда при каждом акте считывания формы \lstinline{DEFPACKAGE} эти
символы интернировались бы в текущий пакет, что, по меньшей мере, загрязняло бы его
пространство имён и могло бы в дальнейшем привести к проблемам при использовании
пакета\footnote{Некоторые парни вместо ключевых слов используют внепакетные символы
посредством синтаксиса \lstinline!#:!.
\begin{myverb}
(defpackage #:com.gigamonkeys.email-db
(:use #:common-lisp))
\end{myverb}
Это слегка экономит память, потому что не вводит никаких символов в пакет
\lstinline{KEYWORD},~--- символ может стать мусором после того, как \lstinline{DEFPACKAGE} (или код,
в который он раскрывается) отработает с ним. Однако экономия столь мала, что в конце
концов всё сводится к вопросу эстетики.}\hspace{\footnotenegspace}.
Чтобы прочесть код в этом пакете, вы должны сделать его текущим пакетом с помощью макроса
\lstinline{IN-PACKAGE}:
\begin{myverb}
(in-package :com.gigamonkeys.email-db)
\end{myverb}
Если вы напечатаете это выражение в REPL, оно изменит значение \lstinline{*PACKAGE*} и повлияет
на то, как REPL будет читать последующие выражения до тех пор, пока вы не измените это
другим вызовом \lstinline{IN-PACKAGE}. Точно так же, если вы включите \lstinline{IN-PACKAGE} в файл,
который загрузите посредством \lstinline{LOAD} или скомпилируете посредством
\lstinline{COMPILE-FILE}, это изменит пакет, влияя на то, как последующие выражения будут
читаться из этого файла\footnote{Смысл использования \lstinline{IN-PACKAGE}, вместо того
чтобы просто сделать \lstinline{SETF} для \lstinline{*PACKAGE*}, в том, что \lstinline{IN-PACKAGE}
раскроется в код, который запустится, когда файл будет компилироваться
\lstinline{COMPILE-FILE} так же, как и когда файл загружается через \lstinline{LOAD}, изменяя поведение
процедуры чтения при чтении остатка файла при компиляции.}\hspace{\footnotenegspace}.
Установив текущим пакетом \lstinline{COM.GIGAMONKEYS.EMAIL-DB}, вы можете, кроме имён,
унаследованных от пакета \lstinline{COMMON-LISP}, использовать любые имена, какие вы хотите,
для любых целей. Таким образом, вы можете определить новую функцию hello-world, которая
будет сосуществовать с функцией hello-world, ранее определённой в
\lstinline{COMMON-LISP-USER}. Вот как ведёт себя существующая функция:
\begin{myverb}
CL-USER> (hello-world)
hello, world
NIL
\end{myverb}
Теперь можно переключиться в новый пакет с помощью \lstinline{IN-PACKAGE}\footnote{В~REPL
буфере SLIME можно также изменять пакеты с помощью клавиатурных сокращений REPL. Наберите
запятую и затем введите \lstinline{change-package} в приглашении Command:}. Заметьте, как изменилось
приглашение~--- точная форма зависит от реализации окружения разработки, но в SLIME
приглашение по умолчанию состоит из аббревиатуры имени пакета.
\begin{myverb}
CL-USER> (in-package :com.gigamonkeys.email-db)
#<The COM.GIGAMONKEYS.EMAIL-DB package>
EMAIL-DB>
\end{myverb}
Вы можете определить новую hello-world в этом пакете:
\begin{myverb}
EMAIL-DB> (defun hello-world () (format t "hello from EMAIL-DB package~%"))
HELLO-WORLD
\end{myverb}
И протестировать её вот так:
\begin{myverb}
EMAIL-DB> (hello-world)
hello from EMAIL-DB package
NIL
\end{myverb}
Переключитесь теперь обратно в \lstinline{CL-USER}.
\begin{myverb}
EMAIL-DB> (in-package :cl-user)
#<The COMMON-LISP-USER package>
CL-USER>
\end{myverb}
Со старой функцией ничего не случилось.
\begin{myverb}
CL-USER> (hello-world)
hello, world
NIL
\end{myverb}
\section{Упаковка библиотек для повторного использования}
Во время работы над базой данных почтовых сообщений вы можете написать несколько функций,
относящихся к сохранению и извлечению текста, но в которых нет ничего конкретного для
работы именно с почтой. Вы можете осознать, что эти функции могут быть полезны в других
программах, и решить перепаковать их в библиотеку. Вы должны будете определить новый пакет
и в то же время экспортировать некоторые имена, чтобы сделать их доступными другим
пакетам.
\begin{myverb}
(defpackage :com.gigamonkeys.text-db
(:use :common-lisp)
(:export :open-db
:save
:store))
\end{myverb}
Итак, вы используете пакет \lstinline{COMMON-LISP}, потому что внутри
\lstinline{COM.GIGAMONKEYS.TEXT-DB} вам понадобится доступ к стандартным функциям. Пункт
:export определяет имена, которые будут внешними в \lstinline{COM.GIGAMONKEYS.TEXT-DB} и,
таким образом, доступными в пакетах, которые будут использовать (\lstinline{:use}) его. Следовательно,
после определения этого пакета вы можете изменить определение главного пакета программы
на следующее:
\begin{myverb}
(defpackage :com.gigamonkeys.email-db
(:use :common-lisp :com.gigamonkeys.text-db))
\end{myverb}
Теперь код, записанный в \lstinline{COM.GIGAMONKEYS.EMAIL-DB}, может использовать
неспециализированные имена для экспортированных символов из \lstinline{COMMON-LISP} и
\lstinline{COM.GIGAMONKEYS.TEXT-DB}. Все прочие имена будут продолжать добавляться в пакет
\lstinline{COM.GIGAMONKEYS.EMAIL-DB}.
\section{Импорт отдельных имён}
Предположим теперь, что вы нашли стороннюю библиотеку функций для манипуляций с почтовыми
сообщениями. Имена, использованные в API библиотеки, экспортированы в пакете
\lstinline{COM.ACME.EMAIL} так, что вы могли бы сделать \lstinline{:use} на этот пакет, чтобы
получить доступ к этим именам. Однако, предположим, вам нужна только одна функция из этой
библиотеки, а другие экспортированные в ней символы конфликтуют с именами, которые вы уже
используете (или собираетесь использовать) в вашем собственном коде\footnote{Во время
разработки, если вы пытаетесь сделать \lstinline{:use} на пакет, который экспортирует символы
с такими же именами, как и символы, уже помещённые в использующиеся пакеты, Lisp подаст
сигнал об ошибке и, обычно, предложит вам перезапуск, что приведёт к выбрасыванию
проблемных символов из добавляемого пакета. Детали смотрите в
разделе~\ref{sec:21-pitfalls}.}\hspace{\footnotenegspace}. В~таком случае вы можете импортировать этот единственный
нужный вам символ с помощью пункта \lstinline{:import-from} в \lstinline{DEFPACKAGE}. Например, если
имя нужной вам функции \lstinline{parse-email-address}, вы можете изменить \lstinline{DEFPACKAGE} на
такой:
\begin{myverb}
(defpackage :com.gigamonkeys.email-db
(:use :common-lisp :com.gigamonkeys.text-db)
(:import-from :com.acme.email :parse-email-address))
\end{myverb}
Теперь, где бы имя \lstinline{parse-email-address} ни появилось в коде, прочитанном из пакета
\lstinline{COM.GIGAMONKEYS.EMAIL-DB}, оно будет прочитано как символ из
\lstinline{COM.ACME.EMAIL}. Если надо импортировать более чем один символ из пакета, можно
включить несколько имён после имени пакета в один пункт \lstinline{:import-from}.
\lstinline{DEFPACKAGE} также может включать несколько пунктов \lstinline{:import-from} для импорта
символов из разных пакетов.
По воле случая вы можете попасть и в противоположную ситуацию~--- пакет экспортирует кучу
имён, которые вам нужны, кроме нескольких. Вместо того чтобы перечислять все символы,
которые вам нужны, в пункте \lstinline{:import-from}, лучше сделать \lstinline{:use} на этот пакет и
затем перечислить имена, которые не нужны для наследования в пункте
\lstinline{:shadow}. Предположим, например, что пакет \lstinline{COM.ACME.TEXT} экспортирует кучу
имён функций и классов, нужных в обработке текста. Далее положим, что большая часть этих
функций и классов нужна вам в вашем коде, но одно имя, \lstinline{build-index}, конфликтует с
уже вами задействованным именем. Можно сделать \lstinline{build-index} из \lstinline{COM.ACME.TEXT}
недоступным через его сокрытие.
\begin{myverb}
(defpackage :com.gigamonkeys.email-db
(:use
:common-lisp
:com.gigamonkeys.text-db
:com.acme.text)
(:import-from :com.acme.email :parse-email-address)
(:shadow :build-index))
\end{myverb}
Пункт \lstinline{:shadow} приведёт к созданию нового символа с именем \lstinline{BUILD-INDEX} и
добавлению его прямо в таблицу имя-символ в \lstinline{COM.GIGAMONKEYS.EMAIL-DB}. Теперь, если
процедура чтения прочтёт имя \lstinline{BUILD-INDEX}, он переведёт его в символ из таблицы
\lstinline{COM.GIGAMONKEYS.EMAIL-DB}, вместо того чтобы, в ином случае, наследовать его из
\lstinline{COM.ACME.TEXT}. Этот новый символ также добавляется в список скрывающих символов,
который является частью пакета \lstinline{COM.GIGAMONKEYS.EMAIL-DB}, так что если вы позже
задействуете другой пакет, который тоже экспортирует символ \lstinline{BUILD-INDEX}, пакетная
система будет знать, что тут нет конфликта и вы хотите, чтобы символ из
\lstinline{COM.GIGAMONKEYS.EMAIL-DB} использовался вместо любого другого символа с таким же
именем, унаследованного из другого пакета.
Похожая ситуация может возникнуть, если вы захотите задействовать два пакета, которые
экспортируют одно и то же имя. В~этом случае процедура чтения не будет знать, какое
унаследованное имя использовать, когда он прочтёт это имя в тексте. В~такой ситуации вы
должны исправить неоднозначность путём сокрытия конфликтного имени. Если вам не нужно имя
ни из одного пакета, вы можете скрыть его с по\-мощью пункта \lstinline{:shadow}, создав новый
символ с таким же именем в вашем пакете. Но если вы всё же хотите использовать один из
наследуемых символов, тогда вам надо устранить неоднозначность с помощью пункта
\lstinline{:shadowing-import-from}. Так же как и пункт \lstinline{:import-from}, пункт
\lstinline{:shadowing-import-from} состоит из имени пакета, за которым следуют имена,
импортируемые из этого пакета. Например, если \lstinline{COM.ACME.TEXT} экспортирует имя
\lstinline{SAVE}, которое конфликтует с именем, экспортированным
\lstinline{COM.GIGAMONKEYS.TEXT-DB}, можно устранить неоднозначность следующим
\lstinline{DEFPACKAGE}:
\begin{myverb}
(defpackage :com.gigamonkeys.email-db
(:use
:common-lisp
:com.gigamonkeys.text-db
:com.acme.text)
(:import-from :com.acme.email :parse-email-address)
(:shadow :build-index)
(:shadowing-import-from :com.gigamonkeys.text-db :save))
\end{myverb}
\section{Пакетная механика}
До этого объяснялись основы того, как использовать пакеты для управления пространством
имён в некоторых распространённых ситуациях. Однако ещё один уровень использования
пакетов, который стоит обсудить,~--- неустоявшиеся механизмы управления с кодом, который
использует различные пакеты. В~этом разделе я расскажу о некоторых правилах <<правой
руки>>, о том, как организовать код, где поместить ваши формы \lstinline{DEFPACKAGE},
относящиеся к коду, который использует ваши пакеты через \lstinline{IN-PACKAGE}.
Так как пакеты используются процедурой чтения, пакет должен быть определён до того, как вы
сможете сделать \lstinline{LOAD} на него или сделать \lstinline{COMPILE-FILE} над файлом, который
содержит выражение \lstinline{IN-PACKAGE}, переключающее на тот пакет. Пакет также должен быть
определён до того, как другие формы \lstinline{DEFPACKAGE} смогут ссылаться на него. Например,
если вы собираетесь указать \lstinline{:use COM.GIGAMONKEYS.TEXT-DB} в
\lstinline{COM.GIGAMONKEYS.EMAIL-DB}, то \lstinline{DEFPACKAGE} для \lstinline{COM.GIGAMONKEYS.TEXT-DB}
должен быть выполнен раньше, чем \lstinline{DEFPACKAGE} для \lstinline{COM.GIGAMONKEYS.EMAIL-DB}.
Лучшим первым шагом для того, чтобы убедиться, что пакеты будут существовать тогда, когда
они понадобятся, будет поместить все ваши \lstinline{DEFPACKAGE} в файлы отдельно от кода,
который должен быть прочитан в тех пакетах. Некоторые парни предпочитают создавать файлы
\lstinline{foo-package.lisp} для каждого пакета в отдельности, другие делают единый файл
\lstinline{packages.lisp}, который содержит все \lstinline{DEFPACKAGE} формы для группы родственных
пакетов. Любой метод разумен, хотя метод <<один файл на пакет>> также требует, чтобы вы
выстроили загрузку файлов в правильном порядке в соответствии с межпакетными
зависимостями.
В~любом случае, как только все формы \lstinline{DEFPACKAGE} отделены от кода, который будет в
них прочитан, вы должны выстроить \lstinline{LOAD} файлов, содержащих \lstinline{DEFPACKAGE}, перед
тем как вы будете компилировать или загружать любые другие файлы. Для простых программ
это можно сделать руками: просто \lstinline{LOAD} на файл или файлы, содержащие формы
\lstinline{DEFPACKAGE}, возможно, сперва компилируя их с помощью \lstinline{COMPILE-FILE}. Затем
\lstinline{LOAD} на файлы, которые используют те пакеты, также, если надо, сперва компилируя их
через \lstinline{COMPILE-FILE}. Заметьте, однако, что пакет не существует до тех пор, пока вы
не сделали \lstinline{LOAD} его определения в виде исходного текста или скомпилированной
версии, созданной \lstinline{COMPILE-FILE}. Таким образом, если вы компилируете всё, вы должны
по-прежнему делать \lstinline{LOAD} определениям пакетов, перед тем как вы сможете сделать
\lstinline{COMPILE-FILE} какому-нибудь файлу, который в этих пакетах читается.
Проделывание всех этих операций руками со временем утомляет. Для простых программ можно
автоматизировать все шаги с помощью файла \lstinline{load.lisp}, который будет содержать
подходящие вызовы \lstinline{LOAD} и \lstinline{COMPILE-FILE} в нужном порядке. Затем можно прос\-то
сделать \lstinline{LOAD} этому файлу. Для более сложных программ вы захотите использовать
средство \textit{системных определений} для управления загрузкой и компиляцией файлов в
правильном порядке\pclfootnote{Код для глав <<Практикума>>, доступный с веб-страницы этой
книги, использует библиотеку системных определений ASDF. ASDF расшифровывается как
<<Another System Definition Facility>> (ещё одно или иное средство системных определений).}.
Ещё одно ключевое правило <<правой руки>>~--- это то, что каждый файл должен содержать только
одну форму IN-PACKAGE, и это должна быть первая форма в файле, отличная от комментариев.
Файлы, содержащие формы \lstinline{DEFPACKAGE}, должны начинаться с
\lstinline{(in-package "COMMON-LISP-USER")}, и все другие файлы должны содержать \lstinline{IN-PACKAGE} для одного из
ваших пакетов.
Если вы нарушите это правило и переключите пакет в середине файла, человек, читающий файл,
будет в растерянности, если он не заметит, где случился второй \lstinline{IN-PACKAGE}. Также
многие среды разработки Lisp, в частности такая, как SLIME, основанная на Emacs, ищут
\lstinline{IN-PACKAGE}, чтобы определить пакет, который им надо использовать для общения с
Common Lisp. Множественные формы \lstinline{IN-PACKAGE} в одном файле приводят в растерянность
такие инструменты.
С другой стороны, всё хорошо, если есть несколько файлов, читающихся в одном и том же
пакете, каждый с одинаковой формой\lstinline{IN-PACKAGE}. Это просто вопрос того, как вам
следует организовывать свой код.
Другая часть пакетной механики имеет дело с тем, как именовать пакеты. Пакетные имена
живут в плоском пространстве имён: имена пакетов~--- это просто строки, и различные пакеты
должны иметь текстуально отличные имена. Таким образом, вам надо учитывать возможность
конфликта между именами пакетов. Если вы ис\-поль\-зуе\-те пакеты, которые сами же и
разрабатываете, то, возможно, и обойдётесь короткими именами для своих пакетов. Однако если
вы планируете использовать библиотеки третьих лиц или публиковать свой код для
использования другими программистами, вам надо следовать соглашениям для имён, которые
минимизируют возможность коллизии имён для различных пакетов. Многие Lisp-программисты в
наше время взяли на вооружение Java-стиль в именах наподобие того, что вы видели в этой
главе, состоящий из обращённых доменных имён Интернета, с последующей точкой и строкой
описания.
\section{Пакетные ловушки}
\label{sec:21-pitfalls}
Как только вы освоитесь с созданием пакетов, вы больше не будете тратить времени на
размышления про них. В~них нет ничего такого. Однако пара ловушек, которые часто
подстерегают новичков в Lisp, заставляет пакетную систему казаться сложнее и
недружественнее, чем она есть на самом деле.
Первая ловушка чаще всего проявляется во время работы с REPL. Вы будете искать
какую-нибудь библиотеку, определяющую некоторые нужные функции. Вы попытаетесь вызвать
одну из них так:
\begin{myverb}
CL-USER> (foo)
\end{myverb}
\noindent{}и вас отбросит в отладчик с ошибкой:
\begin{myverb}
attempt to call `FOO' which is an undefined function.
[Condition of type UNDEFINED-FUNCTION]
Restarts:
0: [TRY-AGAIN] Try calling FOO again.
1: [RETURN-VALUE] Return a value instead of calling FOO.
2: [USE-VALUE] Try calling a function other than FOO.
3: [STORE-VALUE] Setf the symbol-function of FOO and call it again.
4: [ABORT] Abort handling SLIME request.
5: [ABORT] Abort entirely from this (lisp) process.
\end{myverb}
\begin{verbatim}
(попытка вызвать `FOO', которая является неопределённой функцией
[Случай типа UNDEFINED-FUNCTION]
Перезапуск:
0: [TRY-AGAIN] Попытаться вызвать FOO снова.
1: [RETURN-VALUE] Возвратить значение вместо вызова FOO.
2: [USE-VALUE] Попытаться вызвать функцию, другую чем FOO.
3: [STORE-VALUE] Setf символ-функцию FOO и вызвать снова.
4: [ABORT] Прервать обработку SLIME запроса.
5: [ABORT] Прервать полностью этот процесс (Lisp).
)
\end{verbatim}
Ну конечно~--- вы забыли использовать пакет библиотеки. Итак, вы выходите из отладчика и
пытаетесь сделать \lstinline{USE-PACKAGE} на библиотечный пакет в надежде получить доступ к
имени \lstinline{FOO}, чтобы можно было вызвать эту функцию.
\begin{myverb}
CL-USER> (use-package :foolib)
\end{myverb}
Но это снова приводит вас к попаданию в отладчик с сообщением об ошибке:
\begin{myverb}
Using package `FOOLIB' results in name conflicts for these symbols: FOO
[Condition of type PACKAGE-ERROR]
Restarts:
0: [CONTINUE] Unintern the conflicting symbols from the `COMMON-LISP-USER' package.
1: [ABORT] Abort handling SLIME request.
2: [ABORT] Abort entirely from this (lisp) process.
\end{myverb}
\begin{verbatim}
Использование пакета `FOOLIB' приводит к конфликту имён для этих символов: FOO
[ Условие типа PACKAGE-ERROR]
Перезапуск:
0: [CONTINUE] Вывести конфликтующие символы из пакета `COMMON-LISP-USER'.
1: [ABORT] Прервать обработку SLIME запроса.
2: [ABORT] Прервать полностью этот процесс (Lisp).
\end{verbatim}
Что такое? Проблема в том, что в первую попытку вызвать \lstinline{foo} процедура чтения прочла имя
\lstinline{foo}, интернировал его в \lstinline{CL-USER}, перед тем как интерпретатор получил
управление и обнаружил, что только что введённое имя не является именем функции. Этот
новый символ затем и законфликтовал с таким же именем, экспортированным из пакета
\lstinline{FOOLIB}. Если бы вы вспомнили о \lstinline{USE-PACKAGE FOOLIB} перед тем, как попытались
вызвать \lstinline{foo}, процедура чтения читала бы \lstinline{foo} как унаследованный символ и не вводил
бы \lstinline{foo} символ в \lstinline{CL-USER}.
Однако ещё не всё потеряно, потому что первый же перезапуск, предлагаемый отладчиком,
исправит ситуацию правильным образом: он выведет символ \lstinline{foo} из
\lstinline{COMMON-LISP-USER}, возвращая пакет \lstinline{CL-USER} обратно в состояние, в котором он
был до вашего вызова \lstinline{foo}, позволит \lstinline{USE-PACKAGE} сделать своё дело и дать
возможность унаследованной \lstinline{foo} стать доступной в \lstinline{CL-USER}.
Такого рода проблемы могут возникать, когда загружаются и компилируются файлы. Например,
если вы определили пакет \lstinline{MY-APP} для кода, предназначенного для использования
функций с именами из пакета \lstinline{FOOLIB}, но забыли сделать \lstinline{:use FOOLIB}, когда
компилируете файлы с \lstinline{(in-package :my-app)} внутри, процедура чтения введёт новые символы
в \lstinline{MY-APP} для имён, которые предполагались быть прочитанными как символы из
\lstinline{FOOLIB}. Когда вы попытаетесь запустить скомпилированный код, вы получите ошибки о
неопределённых функциях. Если вы затем попробуете переопределить пакет \lstinline{MY-APP},
добавив \lstinline{:use FOOLIB}, то получите ошибку конфликта символов. Решение то же самое:
выберите перезапуск с выводом конфликтующих символов из \lstinline{MY-APP}. Затем вам надо
будет перекомпилировать код в пакете \lstinline{MY-APP}, и он будет ссылаться на унаследованные
имена.
Очередная ловушка представляет собой предыдущую наоборот. В~её случае у вас есть
определённый пакет~--- назовём его снова \lstinline{MY-APP},~--- который использует другой
пакет, скажем \lstinline{FOOLIB}. Теперь вы начинаете писать код в пакете \lstinline{MY-APP}. Хотя
вы использовали \lstinline{FOOLIB}, чтобы иметь возможность ссылаться на функцию \lstinline{foo},
\lstinline{FOOLIB} может также экспортировать и другие символы. Если вы используете один из
таких символов~--- скажем, \lstinline{bar}~--- как имя функции в вашем собственном коде, Lisp
не станет возмущаться. Вместо этого имя вашей функции будет символом, экспортированным из
\lstinline{FOOLIB}, и функция перекроет предыдущее определение \lstinline{bar} из \lstinline{FOOLIB}.
Эта ловушка гораздо более коварна, потому что она не вызывает появления ошибки~--- с точки
зрения интерпретатора, это просто запрос на ассоциацию новой функции со старым именем,
нечто вполне законное. Это подозрительно только потому, что код, делающий переопределение,
был прочитан со значением \lstinline{*PACKAGE*}, отличным от пакета данного имени. Но
интерпретатору не обязательно знать об этом. Однако в большинстве реализаций Lisp вы получите
предупреждение про <<переопределение \lstinline{BAR}, сначала определённого в ?>>. Надо быть
внимательным к таким предупреждениям. Если вы перекрыли определение из библиотеки, можете
восстановить его, перезагрузив код библиотеки через \lstinline{LOAD}\footnote{Некоторые
реализации Common Lisp, такие как Allegro и SBCL, предоставляют средство для
<<блокировки>> символов в нужных пакетах, так что они могут быть использованы в
определяющих формах типа \lstinline{DEFUN}, \lstinline{DEFVAR} и \lstinline{DEFCLASS}, только когда их
домашним пакетом является текущий пакет.}\hspace{\footnotenegspace}.
Последняя, относящаяся к пакетам ловушка относительно тривиальна, но на неё попадаются
большинство Lisp-программистов как минимум несколько раз: вы определяете пакет, который
использует \lstinline{COMMON-LISP} и, возможно, несколько библиотек. Затем в REPL вы переходите
в этот пакет, чтобы поиграться. После этого вы решили покинуть Lisp и пробуете вызвать
\lstinline{(quit)}. Однако \lstinline{quit} не имя из пакета \lstinline{COMMON-LISP}~--- оно определено в
зависимости от реализации в некотором определяемом реализацией пакете, который, оказывается,
используется пакетом \lstinline{COMMON-LISP-USER}. Решение просто~--- смените пакет обратно на
\lstinline{CL-USER} для выхода. Или используйте SLIME REPL сокращение для выхода, что к тому
же убережёт вас от необходимости помнить, что в некоторых реализациях Common Lisp
функцией для выхода является \lstinline{exit}, а не \lstinline{quit}.
Вы почти закончили свой тур по Common Lisp. В~следующей главе я расскажу о деталях
расширенного макроса \lstinline{LOOP}. После этого остаток книги посвящён <<практикам>>:
спам-фильтру, библиотеке для разбора двоичных файлов и различным частям потокового MP3-сервера
с веб-интерфейсом.
%%% Local Variables:
%%% mode: latex
%%% TeX-master: "pcl-ru"
%%% TeX-open-quote: "<<"
%%% TeX-close-quote: ">>"
%%% End: