|
|
В.ВодолазкийПервые шаги в Common Lisp - Структуры данных
В Common Lisp реализован удобный механизм создания записей, содержащих именованные компоненты. Это позволяет определить пользователю новые типы данных; каждый символ такого типа будет содержать все компоненты, определенные при задании типа. При определении структур автоматически создаются конструктор всей структуры, функции доступа к отдельным компонентам как на чтение, так и на запись, а также предикат, проверяющий принадлежность символа к структуре данного типа.
Введение в структуры
Механизм структур в Common Lisp почти полностью скрывается в
одном единственном макросе -- Начнем с примера. Предположим, что вы разрабатываете очередную версию Стар-трека -- одну из первых компьютерных игр, но на этот раз на Лиспе. Одна из задач, которая стоит перед вами, состоит в управлении космическими кораблями, которые перемещаются в двумерном координатном пространстве. Понятно, что корабль должен представляться Лисп-объектом некоторого типа. В нашей игре каждый корабль будет характеризоваться текущими координатами (в виде пары x и y), скоростью (представляемой в виде проекций вдоль осей x и y axes), и массой корабля. В результате корабль может быть представлен как запись, имеющая пять составляющих: x-координата, y-координата, x-скорость, y-скорость, и масса. Эта структура, в свою очередь, может быть реализована в Лиспе несколькими способами. Во-первых, это может быть список, состоящий из пяти элементов -- в этом случае x-координата извлекается с помощбю функции car, y-координата -- с помощью cadr, и так далее. Аналогичным образом можно попытаться использовать пятиэлементный вектор: x-координата будет храниться в элементе с номером 0, координата y -- в элементе с номером 1, и так далее. Однако при использовании любого из этих представлений возникает общая проблема, состоящая в том, что компоненты занимают внутри объекта позиции, выбираемые достаточно произвольным образом, что довольно сложно запомнить, и что, в конце концов, приводит к различным ошибкам.
Например, если взглянуть на запись
В идеальном случае компоненты таких структур должны иметь имена.
Например, достаточно удобно использовать конструкцию:
(list 0 0 0 0 0)
В самом деле, разве не удобнее определить
Сам по себе вызов (defstruct ship x-position y-position x-velocity y-velocity mass)
В результате мы сможем создавать произвольное количество
кораблей, как объекты типа
Этот простой пример наглядно демонстрирует мощь и красоту макроса
Как использовать defstruct с максимальной пользойАбсолютно все структуры в Common Lisp определяются с помощью конструкцииdefstruct . Вызов defstruct
определяет новый тип данных, все экземпляры которого содержат
один итот же набор именованных компонентов.
Макрос defstructdefstruct name-and-options [doc-string] slot-description*
Эта макрокоманда предназначена для определения структур данных в
Common Lisp. Наиболее распространенное использование (defstruct ( name option-1 option-2 ... option-m) doc-string slot-description-1 slot-description-2 ... slot-description-n)Аргумент name должен быть символом; он становится именем нового типа данных, состоящего из всех экземпляров структуры. Функция typep после определения структуры сможет
распознавать и корректно обрабатывать это имя.
Это же имя name возвращается в качестве результата выполнения формы defstruct.
Впрочем, в наиболее распространенных случаях никакие
дополнительные параметры не нужны вообще. При этом вы
можете написать просто имя name, вместо
Если при описании структуры используется необязательная строка
документирования doc-string, она присоединяется к типу
name, как строка документации типа
Каждое из описаний компонентов slot-description-j имеет следующий вид: ( slot-name default-init slot-option-name-1 slot-option-value-1 slot-option-name-2 slot-option-value-2 ... slot-option-name-k slot-option-value-k)
При этом каждый slot-name должен представлять собой символ;
именно это позволяет создать для каждого слота (компонента)
соответствующую функцию доступа. Если же при определении
структуры не заданы ни параметры ни объявление
default-init, то вы можете просто написать
slot-name вместо Форма default-init оценивается всякий раз при создании структуры; полученное значение используется в качестве начального значения соответствующего компонента. Эта форма оценивается только в том случае, если соответствующие начальные значения не передаются конструктору структуры явным образом. Если при определении структуры default-init задана не была, то начальные значения компонентов, по большому счету не определены и зависят от реализации системы. В ряде случаев может оказаться удобным использовать объявление структур вообще без каких-либо компонентов. Такие определения, несмотря на кажущуюся абсурдность, оказываются полезными в тех случаях, когда используется параметр :include, конечно, совместно с другими параметрами. Например, вы можете иметь две структуры, которые в целом подобны друг другу, за исключением того, что по-разному выводятся на печать (потому что имеют различные реализации функции :print-function). Макрос defstruct не только определяет функции доступа для каждого компонента, но также настраивает setf для корректной работы с такими функциями доступа, а также определяет предикат name-p, функцию конструктора make-name, и функцию копировщика copy-name. Все имена автоматически созданных функций становятся домашними для того пакета, который является текущим в момент вызова defstruct Кроме того, все такие функции могут быть объявлены с помощью inline, что позволяет несколько повысить эффективность генерации кода. Этот режим используется по умолчанию, и если вы не хотите, чтобы какие-либо функции создавались inline, то за формой defstruct должно следовать объявление notinline, которое перекрывает старое, созданное по умолчанию. Однако, это не слишком безопасное решение. Во многих реализациях Common Lisp результаты переопределения defstruct, то есть повторные вызовы defstruct с одним и тем же именем могут давать непредсказуемые результаты. Причина такого поведения состоит в том, что экземпляры структур, созданных со старыми и новыми определениями могут оказаться несовместимыми друг с другом. А если функции, ассоциированные со старым определением были объявлены inline, и скомпилированы в исполняемый код, то они останутся доступными и после нового определения, в результате чего код станет несовместим с новым определением структуры. Конечно, такое ограничение оказывает влияние на разработку и процесс отладки. Большинство программных сред Lisp, что называется "в курсе" проблем переопределения defstruct, и предусматривают возможность их переопределения, хотя и с генерацией предупреждающих сообщений о возможных конфликтах с другими частями программной среды. Однако, к моменту выпуска готового программного обеспечения все эти конфликты должны быть улажены разработчиком -- в работающих программах подобные разногласия недопустимы! Использование автоматически порожденной функции конструктораПосле того, как вы с помощью defstruct определили новую структуру, вы можете создавать экземпляры структур такого типа с помощью функции конструктора. По умолчанию defstruct автоматически определяет такую функцию и генерирует для нее уникальное имя.Например, для структуры с названием foo, автоматически создается конструктор с названием make-foo; впрочем вы можете задать и другое имя, передав его ключевому аргументу :constructor, или указать, что вы не нуждаетесь в конструкторе вообще, использовав вместо имени конструктора NIL. В общем случае обращение к функции конструктора имеет вид: (name-of-constructor-function slot-keyword-1 form-1 slot-keyword-2 form-2 ...) Все аргументы являются ключевыми. Каждое из ключевых слов slot-keyword должно совпадать с именем компонента структуры. Оцениваются все ключевые слова ...keyword... и формы form.... Коротко говоря, это выглядит так, как если бы функция конструктора получила все свои аргументы как параметры {&key}. Например, структура ship, содержит функцию конструктора, которая получает аргументы с помощью некоторого эквивалента вызова (defun make-ship (&key x-position y-position x-velocity y-velocity mass) \\ ...) Если slot-keyword-j обозначает название компонента, то каждый элемент созданной структуры будет при инициализации получать значение формы form-j. Если же для данного компонента нет пары slot-keyword-j и form-j, значение компонента инициализируется формой default-init, заданной для этого компонента при вызове defstruct. Другими словами, инициализация, заданная в defstruct, отменяется, если используется специальный вызов инициализатора в форме конструктора. Если используется форма инициализации по умолчанию, то она оценивается во время выполнения конструктора, но в лексическом контексте defstruct-формы, в которой она была определена. Если в самом вызове defstruct также не предусмотрен никакой специальный код для инициализации, то начальное значение элементов остается неопределенным. Впрочем, вы всегда можете провести инициализацию, либо в вызове defstruct, либо при вызове функции конструктора. Конечно, это имеет смысл только в том случае, если начальные значения компонентов имеют какое-то значение. Каждая мз форм инициализации, заданных для компонентов defstruct, оценивается при каждом вызове функции конструктора. Понятно, что это может использоваться для получения весьма нетривиальных результатов. Например, если в качестве формы инициализации используется форма (gensym) (либо при вызове функции конструктора, либо как форма инициализации по умолчанию), то каждый вызов функции конструктора будет приводить к однократному вызову gensym, который в свою очередь, будет создавать новый символ.
Параметры компонентов defstructКаждое описание компонента slot-description в вызове defstruct может задать один или несколько параметров компонента. Таким параметры состоят из пар, содержащих ключевое слово и значение (которое не является оцениваемой формой, а представляет собой непосредственное значение!). Например: (defstruct ship (x-position 0.0 :type short-float) (y-position 0.0 :type short-float) (x-velocity 0.0 :type short-float) (y-velocity 0.0 :type short-float) (mass *default-ship-mass* :type short-float :read-only t)) Это описание устанавливает, что каждый компонент всегда будет содержать число с плавающей запятой, записанное в коротком формате, а также, что последний компонент не может быть изменен после того, как будет создан очередной экземпляр объекта. Ниже следует полный список поддерживаемых параметров компонентов.
Обратите внимание, что если задано значение по умолчанию, то установить параметр компонента нельзя. Теперь вы можете самостоятельно попробовать применить структуры на практике. А в следующий раз мы еще раз вернемся к старой доброй defstruct, но обсудим более тонкие моменты, которые позволят получить действительно полную власть над структурами Common Lisp.
|
Послать письмо voldemarus@narod.ru
|