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

21. Программирование по-взрослому: Пакеты и Символы

В 4-й главе я рассказывал, как считыватель Lisp переводит текстовые имена в объекты, которые затем передаются вычислителю в виде так называемых символов. Оказывается, что иметь встроенный тип данных, специально для представления имён, очень удобно для многих видов программирования.1) Это, однако, не тема данной главы. В этой главе я расскажу об одном из наиболее явных и практических аспектов работы с именами: как избегать конфликта между независимо разрабатываемыми кусками кода.

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

В Common Lisp эта проблема пространства имён сводится просто к вопросу контроля за тем, как считыватель переводит текстовые имена в символы: если вы хотите чтобы два появления одного и того же имени рассматривались интерпретатором одинаково, вы должны убедиться, что считыватель использует один и тот же символ для представления каждого из них. И наоборот, если нужно, чтобы два имени рассматривались как разные, даже если они совпадают побуквенно, вам надо, чтобы считыватель создал разные символы для представления этих имён.

Как считыватель использует пакеты

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

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

Две ключевые функции, которые считыватель использует для доступа к отображению имя-в-символ в пакете, это FIND-SYMBOL и INTERN. Обе они получают строку и, необязательно, пакет. Если пакет не указывается, на его место подставляется значение глобальной переменной *PACKAGE*, называемой также текущим пакетом.

FIND-SYMBOL ищет в пакете символ с именем, совпадающим со строкой, и возвращает его или NIL, если символ не найден. INTERN также возвратит существующий символ, но если его нет, создаст новый, назначит строку его именем и поместит в пакет.

Большинство имён используемых вами – неспециализированные имена, не содержащие двоеточий. Когда считыватель читает такое имя, он переводит его в символ путём изменения всех не экранированных букв в верхний регистр и передавая полученную строку INTERN. Таким образом каждый раз, когда считыватель читает то же имя в том же пакете, он получает тот же объект-символ. Это важно, так как интерпретатор использует объектное совпадение символов, чтобы определить к какой функции, переменной или другому программному элементу данный символ относится. То есть причина, по которой выражение вида (hello-world) преобразуется в вызов определённой hello-world фукции, это возврат считывателем одного и того же символа, и когда он читает вызов функции, и когда он он читал форму DEFUN, которая эту функцию определяла. Имя, содержащее двоеточие или двойное двоеточие, является пакетно-специализированным именем. Когда считыватель читает пакетно-специализированное имя, он разбивает его в месте двоеточия(й) и берёт первую часть, как имя пакета, а вторую как имя символа. Затем считыватель просматривает соответствующий пакет и использует его для перевода имени в символ-объект.

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

Ещё два аспекта в синтаксисе символов, которые понимает считыватель, это ключевые и внепакетные символы. Ключевые символы записываются как имена, начинающиеся с двоеточия. Такие символы добавляются в пакет, названный KEYWORD , и экспортируются автоматически. Кроме того, когда считыватель добавляет символ в KEYWORD, он также определяет константную переменную с символом в качестве как имени, так и значения. Вот почему вы можете использовать ключевые слова в списке аргументов без кавычки спереди – когда вычисляется их значение, оно оказывается равным им самим. Таким образом:

(eql ':foo :foo) ==> T

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

(symbol-name :foo) ==> "FOO"

Внепакетные символы записываются с поставленными впереди #:. Эти имена (без #:) преобразуются в верхний регистр, как обычно, и затем переводятся в символы, но эти символы не добавляются ни в один пакет; каждый раз, когда считыватель читает имя с #:, он создаёт новый символ. Таким образом:

(eql '#:foo '#:foo) ==> NIL

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

(gensym) ==> #:G3128

Немного про словарь пакетов и символов

Как я упомянул ранее, соответствие между именами и символами, предоставленными пакетом, устроено более гибко, чем простая таблица соответствий. Ядром каждого пакета является поисковая таблица имя-в-символ, но символ в пакете можно сделать доступным через неспециализированное имя и другим путём. Чтобы поговорить об этих иных механизмах, вам понадобятся некоторые термины.

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

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

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

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

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

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

Три стандартных пакета

В следующем разделе я покажу вам как создавать ваши собственные пакеты, включая создание одного пакета с использованием другого и как экспортировать, скрывать и импортировать символы. Но вначале давайте посмотрим на несколько пакетов, которыми вы уже пользовались. Когда вы запускаете Лисп, значением *PACKAGE* обычно является пакет COMMON-LISP-USER, также известный как CL-USER.2) CL-USER использует пакет COMMON-LISP, который экспортирует все имена из языкового стандарта. Таким образом, когда вы набираете выражение в REPL, все имена стандартных функций, макросов, переменных и так далее, будут преобразованы в символы, экспортированные из COMMON-LISP, и все другие имена будут интернированы в пакет COMMON-LISP-USER. Например имя *PACKAGE* экспортировано из COMMON-LISP – если вы хотите увидеть значение *PACKAGE*, наберите следующее:

CL-USER> *package*
#<The COMMON-LISP-USER package>

потому что COMMON-LISP-USER использует COMMON-LISP. Или вы можете задать пакетно-специализированное имя.

CL-USER> common-lisp:*package*
#<The COMMON-LISP-USER package>

Вы даже можете использовать CL, псевдоним COMMON-LISP.

CL-USER> cl:*package*
#<The COMMON-LISP-USER package>

Однако *X* не является символом из COMMON-LISP, так что если вы наберёте:

CL-USER> (defvar *x* 10)
*X*

считыватель прочтёт DEFVAR как символ из пакета COMMON-LISP и *X*, как символ из COMMON-LISP-USER.

REPL не может запускаться в пакете COMMON-LISP, потому что вам не позволено вводить никакие символы в него; COMMON-LISP-USER работает как "черновой" пакет в котором вы можете создавать собственные имена, в то же время имея лёгкий доступ ко всем символам из COMMON-LISP.3) Обычно, все созданные вами пакеты будут так же использовать COMMON-LISP, так что вы не должны писать нечто вроде:

(cl:defun (x) (cl:+ x 2))

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

CL-USER> :a
:A
CL-USER> keyword:a
:A
CL-USER> (eql :a keyword:a)
T

Определение собственных пакетов

Работать в COMMON-LISP-USER замечательно для экспериментов в REPL, но как только вы начнёте писать настоящую программу, вы захотите определить новый пакет, чтобы различные программы, загруженные в одну среду Lisp, не топтались на именах друг друга. И когда вы пишете библиотеку, которую намереваетесь использовать в различных контекстах, вы захотите определить различные пакеты и затем экспортировать символы, которые составляют публичный API библиотеки.

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

Запомнив это, вы можете начать рассмотрение того, как определять пакеты и увязывать их друг с другом. Вы определяете новые пакеты через макрос DEFPACKAGE, который даёт возможность не только создать пакет, но и определить, какие пакеты он будет использовать, какие символы экспортирует, какие символы импортирует из других пакетов, и разрешить конфликты посредством скрытия символов.5)

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

Первый пакет, который вам нужен, это тот, который предоставляет пространство имён для приложений – вы захотите именовать ваши функции, переменные и так далее, без заботы о коллизии имён с не относящимся к делу кодом. Так вы определите новый пакет посредством DEFPACKAGE.

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

(defpackage :com.gigamonkeys.email-db
  (:use :common-lisp)
)

Здесь определяется пакет, названный COM.GIGAMONKEYS.EMAIL-DB, который наследует все символы, экспортируемые пакетом COMMON-LISP.6)

У вас, на самом деле, есть выбор, как представлять имена пакетов и, как вы увидите, имена символов в DEFPACKAGE. Пакеты и символы называются с помощью строк. Однако, в форме DEFPACKAGE, вы можете задать имена пакетов и символов через строковые обозначения. Строковыми обозначениями являются строки, которые обозначают сами себя; символы, которые обозначают свои имена; или знак, который означает однобуквенную строку, содержащую только этот знак. Использование ключевых символов, как в вышеприведённом DEFPACKAGE, является общепризнанным стилем, который позволяет вам писать имена в нижнем регистре – считыватель преобразует для вас имена в верхний регистр. Так же можно записывать DEFPACKAGE с помощью строк, но тогда вы должны писать их все в верхнем регистре, потому, что настоящие имена большинства символов и пакетов фактически в верхнем регистре из-за соглашения о преобразовании, которое выполняет считыватель.7)

(defpackage "COM.GIGAMONKEYS.EMAIL-DB"
  (:use "COMMON-LISP")
)

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

Чтобы прочесть код в этом пакете, вы должны сделать его текущим пакетом с помощью макроса IN-PACKAGE:

(in-package :com.gigamonkeys.email-db)

Если вы напечатаете это выражение в REPL, оно изменит значение *PACKAGE* и повлияет на то, как REPL будет читать последующие выражения до тех пор, пока вы не измените это другим вызовом IN-PACKAGE. Точно также, если вы включите IN-PACKAGE в файл, который загрузите посредством LOAD или скомпилируете посредством COMPILE-FILE, это изменит пакет, влияя на то, как последующие выражения будут читаться из этого файла.9)

Установив текущим пакетом COM.GIGAMONKEYS.EMAIL-DB, вы можете, кроме имён, унаследованных от пакета COMMON-LISP, использовать любые имена, какие вы хотите, для любых целей. Таким образом, вы можете определить новую функцию hello-world, которая будет сосуществовать с функцией hello-world, ранее определённой в COMMON-LISP-USER. Вот как ведёт себя существующая функция:

CL-USER> (hello-world)
hello, world
NIL

Теперь можно переключиться в новый пакет с помощью IN-PACKAGE.10) Заметьте, как изменилось приглашение – точная форма зависит от реализации окружения разработки, но в SLIME приглашение по умолчанию состоит из аббревиатуры имени пакета.

CL-USER> (in-package :com.gigamonkeys.email-db)
#<The COM.GIGAMONKEYS.EMAIL-DB package>
EMAIL-DB>

Вы можете определить новую hello-world в этом пакете:

EMAIL-DB> (defun hello-world () (format t "hello from EMAIL-DB package~%"))
HELLO-WORLD

И протестировать её вот так:

EMAIL-DB> (hello-world)
hello from EMAIL-DB package
NIL

Переключитесь теперь обратно в CL-USER.

EMAIL-DB> (in-package :cl-user)
#<The COMMON-LISP-USER package>
CL-USER>

Со старой функцией ничего не случилось.

CL-USER> (hello-world)
hello, world
NIL

Упаковка библиотек для повторного использования

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

(defpackage :com.gigamonkeys.text-db
  (:use :common-lisp)
  (:export :open-db   
           :save
           :store
)
)

Итак, вы используете пакет COMMON-LISP , потому что внутри COM.GIGAMONKEYS.TEXT-DB вам понадобится доступ к стандартным функциям. Пункт :export определяет имена, которые будут внешними в COM.GIGAMONKEYS.TEXT-DB, и, таким образом, доступными в пакетах, которые будут :use (использовать) его. Следовательно, после определения этого пакета, вы можете изменить определение главного пакета программы на следующее:

(defpackage :com.gigamonkeys.email-db
  (:use :common-lisp :com.gigamonkeys.text-db)
)

Теперь код, записанный в COM.GIGAMONKEYS.EMAIL-DB, может использовать неспециализированные имена для экспортированных символов из COMMON-LISP и COM.GIGAMONKEYS.TEXT-DB. Все прочие имена будут продолжать добавляться в пакет COM.GIGAMONKEYS.EMAIL-DB.

Импорт отдельных имён

Предположим теперь, что вы нашли стороннюю библиотеку функций для манипуляций с почтовыми сообщениями. Имена, использованные в API библиотеки, экспортированы в пакете COM.ACME.EMAIL, так, что вы могли бы сделать :use на этот пакет, чтобы получить доступ к этим именам. Однако, предположим, вам нужна только одна функция из этой библиотеки, а другие экспортированные в ней символы конфликтуют с именами, которые вы уже используете (или собираетесь использовать) в вашем собственном коде.11) В таком случае, вы можете импортировать этот единственный нужный вам символ с помощью пункта :import-from в DEFPACKAGE. Например, если имя нужной вам функции parse-email-address, вы можете изменить DEFPACKAGE на такой:

(defpackage :com.gigamonkeys.email-db
  (:use :common-lisp :com.gigamonkeys.text-db)
  (:import-from :com.acme.email :parse-email-address)
)

Теперь, где бы имя parse-email-address ни появилось в коде, прочитанном из пакета COM.GIGAMONKEYS.EMAIL-DB, оно будет прочитано как символ из COM.ACME.EMAIL. Если надо импортировать более чем один символ из пакета, можно включить несколько имён после имени пакета в один пункт :import-from. DEFPACKAGE также может включать несколько пунктов :import-from для импорта символов из разных пакетов.

По воле случая, вы можете попасть и в противоположную ситуацию – пакет экспортирует кучу имён, которые вам нужны, кроме нескольких. Вместо того, чтобы перечислять все символы, которые вам нужны в пункте :import-from, лучше сделать :use на этот пакет и затем перечислить имена, которые не нужны для наследования в пункте :shadow. Предположим, например, что пакет COM.ACME.TEXT экспортирует кучу имён функций и классов нужных в обработке текста. Далее положим, что большая часть этих функций и классов нужны вам в вашем коде, но одно имя, build-index, конфликтует с уже вами задействованным именем. Можно сделать build-index из COM.ACME.TEXT недоступным через его сокрытие.

(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)
)

Пункт :shadow приведёт к созданию нового символа с именем BUILD-INDEX и добавлению его прямо в таблицу имя-символ в COM.GIGAMONKEYS.EMAIL-DB. Теперь, если считыватель прочтёт имя BUILD-INDEX, он переведёт его в символ из таблицы COM.GIGAMONKEYS.EMAIL-DB, вместо того, чтобы, в ином случае, наследовать его из COM.ACME.TEXT. Этот новый символ также добавляется в список скрывающих символов, который является частью пакета COM.GIGAMONKEYS.EMAIL-DB, так что, если вы позже задействуете другой пакет, который тоже экспортирует символ BUILD-INDEX, пакетная система будет знать, что тут нет конфликта, и вы хотите, чтобы символ из COM.GIGAMONKEYS.EMAIL-DB использовался вместо любого другого символа с таким же именем, унаследованного из другого пакета.

Похожая ситуация может возникнуть, если вы захотите задействовать два пакета, которые экспортируют одно и то же имя. В этом случае считыватель не будет знать какое унаследованное имя использовать, когда он прочтёт это имя в тексте. В такой ситуации вы должны исправить неоднозначность путём сокрытия конфликтного имени. Если вам не нужно имя ни из одного пакета, вы можете скрыть его с помощью пункта :shadow, создав новый символ с таким же именем в вашем пакете. Но если вы всё же хотите использовать один из наследуемых символов, тогда вам надо устранить неоднозначность с помощью пункта :shadowing-import-from. Так же, как и пункт :import-from, пункт :shadowing-import-from состоит из имени пакета за которым следуют имена, импортируемые из этого пакета. Например, если COM.ACME.TEXT экспортирует имя SAVE, которое конфликтует с именем, экспортированным COM.GIGAMONKEYS.TEXT-DB, можно устранить неоднозначность следующим DEFPACKAGE:

(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)
)

Пакетная механика

До этого объяснялись основы того, как использовать пакеты для управления пространством имён в некоторых распространённых ситуациях. Однако ещё один уровень использования пакетов, который стоит обсудить – неустоявшиеся механизмы управления с кодом, который использует различные пакеты. В этом разделе я расскажу о некоторых правилах "правой руки", о том, как организовать код, где поместить ваши формы DEFPACKAGE, относящиеся к коду, который использует ваши пакеты через IN-PACKAGE.

Так как пакеты используются считывателем, пакет должен быть определён до того, как вы сможете сделать LOAD на него или сделать COMPILE-FILE над файлом, который содержит выражение IN-PACKAGE, переключающее на тот пакет. Пакет также должнен быть определен до того, как другие формы DEFPACKAGE смогут ссылаться на него. Например, если вы собираетесь указать :use COM.GIGAMONKEYS.TEXT-DB в COM.GIGAMONKEYS.EMAIL-DB, то DEFPACKAGE для COM.GIGAMONKEYS.TEXT-DB должен быть выполнен раньше, чем DEFPACKAGE для COM.GIGAMONKEYS.EMAIL-DB.

Лучшим первым шагом для того, чтобы убедиться, что пакеты будут существовать тогда, когда они понадобятся, будет поместить все ваши DEFPACKAGE в файлы, отдельно от кода, который должен быть прочитан в тех пакетах. Некоторые парни предпочитают создавать файлы foo-package.lisp для каждого пакета в отдельности, другие делают единый файл packages.lisp, который содержит все DEFPACKAGE формы для группы родственных пакетов. Любой метод разумен, хотя метод "один файл на пакет" также требует, чтобы вы выстроили загрузку файлов в правильном порядке в соответствии с межпакетными зависимостями.

В любом случае, как только все формы DEFPACKAGE отделены от кода, который будет в них прочитан, вы должны выстроить LOAD файлов, содержащих DEFPACKAGE, перед тем, как вы будете компилировать или загружать любые другие файлы. Для простых программ это можно сделать руками: просто LOAD на файл или файлы, содержащие формы DEFPACKAGE, возможно, сперва компилируя их с помощью COMPILE-FILE. Затем LOAD на файлы, которые используют те пакеты, также, если надо, сперва компилируя их через COMPILE-FILE. Заметьте, однако, что пакет не существует до тех пор, пока вы не сделали LOAD его определения в виде исходного текста или скомпилированной версии, созданной COMPILE-FILE. Таким образом, если вы компилируете всё, вы должны по-прежнему делать LOAD определениям пакетов, перед тем, как вы сможете сделать COMPILE-FILE какому-нибудь файлу, читающемуся в тех пакетах (FIXME to be read in the packages).

Проделывание всех этих операций руками со временем утомляет. Для простых программ можно автоматизировать все шаги с помощью файла load.lisp, который будет содержать подходящие вызовы LOAD и COMPILE-FILE в нужном порядке. Затем можно просто сделать LOAD этому файлу. Для более сложных программ вы захотите использовать средство системных определений для управления загрузкой и компиляцией файлов в правильном порядке.12)

Ещё одно ключевое правило "правой руки", это то, что каждый файл должен содержать только одну форму IN-PACKAGE, и это должна быть первая форма в файле, отличная от комментариев. Файлы, содержащие формы DEFPACKAGE, должны начинаться с (in-package "COMMON-LISP-USER"), и все другие файлы должны содержать IN-PACKAGE для одного из ваших пакетов.

Если вы нарушите это правило и переключите пакет в середине файла, человек, читающий файл, будет в растерянности, если он не заметит где случился второй IN-PACKAGE. Также многие среды разработки Лисп, в частности такая, как SLIME, основанная на Emacs, ищут IN-PACKAGE, чтобы определить пакет, который им надо использовать для общения с Common Lisp. Множественные формы IN-PACKAGE в одном файле приводят в растерянность такие инструменты.

С другой стороны, всё хорошо, если есть несколько файлов, читающихся в одном и том же пакете, каждый с одинаковой формойIN-PACKAGE. Это просто вопрос того, как вам следует организовывать свой код.

Другая часть пакетной механики имеет дело с тем, как именовать пакеты. Пакетные имена живут в плоском пространстве имён – имена пакетов это просто строки и различные пакеты должны иметь текстуально отличные имена. Таким образом вам надо учитывать возможность конфликта между именами пакетов. Если вы используете пакеты, которые сами же и разрабатываете, то возможно и обойдётесь короткими именами для своих пакетов. Однако, если вы планируете использовать библиотеки третьих лиц или публиковать свой код для использования другими программистами, вам надо следовать соглашениям для имён, которые минимизируют возможность коллизии имён для различных пакетов. Многие Lisp-программисты в наше время взяли на вооружение Java-стиль в именах, наподобие того, что вы видели в этой главе, состоящие из обращённых доменных имён Интернета, с последующей точкой и строкой описания.

Пакетные ловушки

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

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

CL-USER> (foo)

и вас отбросит в отладчик с ошибкой:

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.

(попытка вызвать `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] Прервать полностью этот  (Лисп) процесс.
)

Ну конечно – вы забыли использовать пакет библиотеки. Итак, вы выходите из отладчика и пытаетесь сделать USE-PACKAGE на библиотечный пакет в надежде получить доступ к имени FOO, чтобы можно было вызвать эту функцию.

CL-USER> (use-package :foolib)

Но это снова приводит вас к попаданию в отладчик с сообщением об ошибке:

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.

Использование пакета `FOOLIB' приводит к конфликту имён для этих символов: FOO
  [ Условие типа PACKAGE-ERROR]

Перезапуск:
 0: [CONTINUE] Вывести конфликтующие символы из пакета `COMMON-LISP-USER'.
 1: [ABORT] Прервать обработку SLIME  запроса.
 2: [ABORT] Прервать полностью этот (Лисп) процесс.

Что такое? Проблема в том, что в первую попытку вызвать foo, считыватель прочёл имя foo интернировал его в CL-USER перед тем, как интерпретатор получил управление и обнаружил, что только что введённое имя не является именем функции. Этот новый символ затем и законфликтовал с таким же именем, экспортированным из пакета FOOLIB. Если бы вы вспомнили о USE-PACKAGE FOOLIB перед тем, как попытались вызвать foo, считыватель читал бы foo как унаследованный символ и не вводил бы foo символ в CL-USER.

Однако, ещё не всё потеряно, потому, что первый же перезапуск, предлагаемый отладчиком, исправит ситуацию правильным образом: он выведет символ foo из COMMON-LISP-USER, возвращая пакет CL-USER обратно в состояние, в котором он был до вашего вызова foo, позволит USE-PACKAGE сделать своё дело и дать возможность унаследованной foo стать доступной в CL-USER.

Такого рода проблемы могут возникать, когда загружаются и компилируются файлы. Например, если вы определили пакет MY-APP для кода, предназначенного для использования функций с именами из пакета FOOLIB, но забыли сделать :use FOOLIB, когда компилируете файлы с (in-package :my-app) внутри, считыватель введёт новые символы в MY-APP для имён, которые предполагались быть прочитанными как символы из FOOLIB. Когда вы попытаетесь запустить скомпилированный код, вы получите ошибки о неопределённых функциях. Если вы затем попробуете переопределить пакет MY-APP, добавив :use FOOLIB, то получите ошибку конфликта символов. Решение то же самое: выберите перезапуск с выводом конфликтующих символов из MY-APP. Затем вам надо будет перекомпилировать код в пакете MY-APP, и он будет ссылаться на унаследованные имена.

Очередная ловушка представляет собой предыдущую наоборот. В её случае у вас есть определённый пакет – назовём его, снова, MY-APP – который использует другой пакет, скажем, FOOLIB. Теперь вы начинаете писать код в пакете MY-APP. Хотя вы использовали FOOLIB, чтобы иметь возможность ссылаться на функцию foo, FOOLIB может также экспортировать и другие символы. Если вы используете один из таких символов – скажем, bar – как имя функции в вашем собственном коде, Lisp не станет возмущаться. Вместо этого, имя вашей функции будет символом, экспортированным из FOOLIB, и функция перекроет предыдущее определение bar из FOOLIB.

Эта ловушка гораздо более коварна, потому что она не вызывает появление ошибки – с точки зрения интерпретатора это просто запрос на ассоциацию новой функции со старым именем, нечто вполне законное. Это подозрительно только потому, что код, делающий переопределение, был прочитан со значением *PACKAGE*, отличным от пакета данного имени. Но интерпретатору не обязательно знать об этом. Однако, во большинстве Лиспов, вы получите предупреждение про "переопределение BAR, сначала определённом в ?". Надо быть внимательным к таким предупреждениям. Если вы перекрыли определение из библиотеки, можете восстановить его, перезагрузив код библиотеки через LOAD.13)

Последняя, относящаяся к пакетам ловушка, относительно тривиальна, но на неё попадаются большинство Lisp-программистов как минимум несколько раз: вы определяете пакет, который использует COMMON-LISP и, возможно, несколько библиотек. Затем в REPL вы переходите в этот пакет чтобы поиграться. После этого вы решили покинуть Lisp и пробуете вызвать (quit). Однако quit не имя из пакета COMMON-LISP – оно определено в зависимости от реализации в некотором определяемом реализацией пакете, который оказывается используется пакетом COMMON-LISP-USER. Решение просто – смените пакет обратно на CL-USER для выхода. Или используйте SLIME REPL сокращение для выхода, что, к тому же, убережёт вас от необходимости помнить, что в некоторых реализациях Common Lisp функцией для выхода является exit, а не quit.

Вы почти закончили свой тур по Common Lisp. В следующей главе я расскажу о деталях расширенного макроса LOOP. После этого, остаток книги посвящён "практикам": спам-фильтру, библиотеке для разбора двоичных файлов, и различным частям потокового MP3 сервера с Веб интерфейсом.

1)Способ программирования, основывающийся на типе данных символ, называется, вполне подходяще, символьной обработкой. Обычно он противопоставляется численному программированию. Пример программы, с которой должны быть хорошо знакомы все программисты и которая занимается почти исключительно символьными преобразованиями - компилятор. Он принимает текст программы, как набор символов и преобразует его в новую форму.
2)У каждого пакета есть одно официальное имя и ноль или больше псевдонимов, которые могут быть использованы везде, где нужно имя пакета. То есть в таких случаях как пакетно-специализированные имена или ссылка на пакет в DEFPACKAGE или IN-PACKAGE формах.
3)COMMON-LISP-USER так же позволено предоставлять доступ к символам, экспортированными некоторыми другими, в зависимости от реализации, пакетами. Хотя это сделано для удобства пользователя – это делает специфическую для каждого воплощения функциональность готовой к употреблению – однако так же служит причиной растерянности для новичков: Лисп будет возмущаться попыткой переопределить некоторые имена, не входящие в стандарт языка. Посмотреть, из каких пакетов COMMON-LISP-USER наследует символы в данной реализации, можно, выполнив следующее выражение в REPL:
(mapcar #'package-name (package-use-list :cl-user))
А чтоб найти, из какого изначального пакета взят символ, выполните это:
(package-name (symbol-package 'some-symbol))
где some-symbol надо заменить на запрашиваемый символ. Например:
(package-name (symbol-package 'car)) ==> "COMMON-LISP"
(package-name (symbol-package 'foo)) ==> "COMMON-LISP-USER"
Символы, унаследованные от пакетов, определённых вашей реализацией, возвратят несколько другие значения.
4)Это отличается от пакетной системы Java, которая предоставляет пространство имён для классов, но также включает Java-механизм контроля доступа. Язык не из семейства Lisp с похожей на Common Lisp пакетной системой - это Perl.
5)Все манипуляции, выполняемые через DEFPACKAGE, так же могут быть выполнены функциями, которые манипулируют пакетными объектами. Однако, так как пакет, вообще говоря, должен быть полностью определён перед тем, как он может быть использован, эти функции редко находят применение. Также, DEFPACKAGE заботится о выполнении всех манипуляций с пакетом в правильном порядке – например DEFPACKAGE добавляет символы в список скрывающих перед тем, как он пытается использовать подключённые пакеты.
6)Во многих реализациях Lisp пункт :use необязателен, если вы хотите просто :use(использовать) COMMON-LISP – если он пропущен, пакет автоматически наследует имена от всего определённого для данного воплощения списка пакетов, который, обычно, включает и COMMON-LISP. Однако ваш код будет чуть более портируем, если вы всегда будете явно указывать все пакеты, которые вы хотите использовать (:use). Те, кому неохота много печатать, могут задействовать псевдонимы и написать (:use :cl).
7)Использование ключевых слов вместо строк также имеет другое преимущество – Allegro предоставляет "современный стиль" Lisp, в котором считыватель не совершает преобразования имён и, в котором, вместо пакета COMMON-LISP с именами в верхнем регистре, предоставляется пакет common-lisp с именами в нижнем. Строго говоря, такой Lisp не удовлетворяет требованиям Common Lisp, так как все имена по стандарту определены в верхнем регистре. Однако, если запишете свою форму DEFPACKAGE, используя ключевые символы, она будет работать как в Common Lisp, так и в его ближайших родственниках.
8)Некоторые парни вместо ключевых слов используют внепакетные символы, посредством #: синтаксиса.
(defpackage #:com.gigamonkeys.email-db
  (:use #:common-lisp)
)

Это слегка экономит память, потому, что не вводит никаких символов в пакет KEYWORD – символ может стать мусором после того, как DEFPACKAGE (или код в который он расширяется) отработает с ним. Однако экономия столь мала, что в конце концов всё сводится к вопросу эстетики.
9)Смысл использования IN-PACKAGE вместо того, чтобы просто сделать SETF для *PACKAGE* в том, что IN-PACKAGE расширится в код, который запустится, когда файл будет компилироваться COMPILE-FILE также, как и когда файл загружается через LOAD, изменяя поведение считывателя при чтении остатка файла при компиляции.
10)В REPL буфере SLIME можно также изменять пакеты с помощью клавиатурных сокращений REPL. Наберите запятую и затем введите change-package в приглашение Command:
11)Во время разработки, если вы пытаетесь сделать :use на пакет, который экспортирует символы с такими же именами, как и символы уже помещённые в использующиеся пакеты, Lisp подаст сигнал об ошибке и, обычно, предложит вам перезапуск, что приведёт к выбрасыванию проблемных символов из добавляемого пакета. Детали смотрите в разделе "Пакетные подводные камни."
12)Код для глав "Практикума", доступный с Веб-страницы этой книги, использует библиотеку системных определений ASDF. ASDF расшифровывается как "Another System Definition Facility" (ещё одно или иное средство системных определений) .
13)Некоторые реализации Common Lisp, такие как Allegro и SBCL, предоставляют средство для "блокировки" символов в нужных пакетах так, что они могут быть использованы в определяющих формах типа DEFUN, DEFVAR и DEFCLASS только когда их домашним пакетом является текущий пакет.
Предыдущая Оглавление Следующая
@2009-2013 lisper.ru