Итак, рассказ о внутренностях ASDF начнем с того, что меня самого испугала бы задача создать с нуля подобную систему. В данном случае в идеале нужно единомоментно получить программу, обладающую одновременно такими довольно противоречивыми характеристиками:
- хорошо покрывающую основные варианты использования (в случае ASDF — это и средство описания систем (для их последующего распространения), и менеджер сборки)
- простую и удобную для непосвященных в детали пользователей
- хорошо расширяемую для того, чтобы позволить развивать сопутствующую инфраструктуру (например, такие средства как ASDF-INSTALL)
- ну и, разумеется, сразу корректно работающую
Что же представляет из себя эта сама по себе довольно большая система изнутри? Хребтом ASDF является иерархия классов
component
→ module
→ system
, которые содержат информацию об именах, местоположении и зависимостях систем и их компонент, метаинформацию, а также служебную информацию самой ASDF.Кроме того описаны классы операций, которые могут выполняться над системами, как то:
compile-op
, load-op
, test-op
и т.д. Почему операции являются объектами, а не просто ключами, что напрашивается на первый взгляд? Во-первых, это позволяет наследовать от них и при этом точечно менять поведение связанных обобщенных функций. Но даже более важно то, что объект операции имеет определенные свойства: операцию-родитель, является ли операция форсированной (хотя это свойство на данный момент не работает корректно), таблицы отработанных и еще не отработанных узлов и т.д. Это имеет и свой недостаток, поскольку кажется несколько избыточным для обычного пользователя. В версии ASDF 2 для его устранения введены функции-обертки load-system
, compile-system
и test-system
.На уровне пользовательского API на основе всех этих классов функционирует макро
defsystem
, а также обобщенная функция operate
.Defsystem
— это хороший инструмент описания систем, знакомый и понятный, я думаю, каждому. Он ведет свою историю еще от Lisp Machine Lisp DEFSYSTEM
, хотя с тех пор и существенно эволюционировал в сторону упрощения интерфейса.Параметры
defsystem
-формы не передаются, как можно было бы предположить, напрямую в (make-instance 'system ...)
, а сперва обрабатываются функцией parse-component-form
. При этом часть параметров передается как есть, а часть транслируется или используется в качестве мета-параметров. Остановлюсь на двух из них:defsystem
можно применять не только для стандартных систем, но и их потомков за счет параметра:class
- параметр
:depends-on
(и:weakly-depends-on
1), на самом деле, не присутствует в качестве слота в классеcomponent
. Его содержимое транслируется в содержимое слотаin-order-to
, которое описывает зависимости более гранулярно отдельно для каждой операции. Кстати, этот слот можно задать напрямую вdefsystem
-описании, чем иногда пользуются при необходимости указания нестандартных сценариев поведения. Впрочем, среди установленных у меня порядка 70 библиотек я нашел только несколько примеров такого использования, самый интересный из которых — в описании системы weblocks-prevalance (в остальных случаях это применяется для указания зависимостей систем-тестовых наборов). В данном случае устанавливается зависимость от дополнительно определенной операцииprepare-prevalance-op
:
(defsystem weblocks-prevalence
:name "weblocks-prevalence"
;; кстати, нет смысла задавать параметр `name' для системы,
;; поскольку имя берется из символа, передаваемого в defsystem
:description "A weblocks backend for cl-prevalence."
:depends-on (:metatilities :cl-ppcre :cl-prevalence :bordeaux-threads)
:components ((:file "prevalence"))
:in-order-to ((compile-op (prepare-prevalence-op :weblocks-prevalence))
(load-op (prepare-prevalence-op :weblocks-prevalence))))
А сама операция prepare-prevalence-op
характеризуется всего одним дополнительным методом, отвечающим за подгрузку дополнительной системы, находящейся по внутреннему пути, не известному в *central-registry*
:
(defmethod perform ((op prepare-prevalence-op)
(c (eql (find-system :weblocks-prevalence))))
(unless (find-package :weblocks-memory)
;; load weblocks if necessary
(unless (find-package :weblocks)
(asdf:oos 'asdf:load-op :weblocks))
;; load weblocks-memory.asd
(load (merge-pathnames
(make-pathname :directory '(:relative "src" "store" "memory")
:name "weblocks-memory" :type "asd")
(funcall (symbol-function
(find-symbol (symbol-name '#:asdf-system-directory)
(find-package :weblocks)))
:weblocks)))
;; load weblocks-memory
(asdf:oos 'asdf:load-op :weblocks-memory)))
Впрочем, это можно было бы сделать и иначе :)Проблемой использования
defsystem
, которой я коснусь в следующей статье на тему шаблонов применения ASDF, является то, что есть соблазн отступить о чисто декларативного описания системы и добавить в него некоторые исполняемые элементы, например, чтение и подстановку версии системы из отдельного файла. Как по мне, было бы разумно обрабатывать эту форму в рамках (let ((*read-evel* nil) ...)
, чтобы исключить такие варианты. Причина тут в том, что ASDF-описание может обрабатыватся более, чем 1 раз при поиске систем и разрешении зависимостей, и работа с ним в таком случае выполняется в режиме просто чтения. Возможно, это ограничение будет со временем установлено: во всяком случае это уже обсуждалось в рассылке.Теперь рассмотрим функцию
operate
, которая является точкой входа в область собственно ядра ASDF, которое отвечает за поиск и выполнение операций над зависимыми компонентами. Она опирается на обобщенные функции traverse
, роль которой — в построении плана выполнения той или иной операции, и perform
, которая собственно выполняет конечные действия, будь то компиляция, загрузка файлов и т.д, а также на функцию find-system
. Кроме того, интересным дополнением (неким альтер-его) perform
является explain
, которая только указывает, какое действие должно быть выполнено. Хорошая иллюстрация возможностей применения explain
дана в статье Питмана:(DOLIST (STEP (SYSDEF:GENERATE-PLAN system :UPDATE))
(SYSDEF:EXPLAIN-ACTION system STEP)
(UNLESS (NOT (Y-OR-N-P "OK? "))
(SYSDEF:EXECUTE-ACTION system STEP)))
Этот код на Lisp Machine Lisp позволяет пошагово выполнять обновление системы при условии согласия пользователя на каждом шаге.Операция
traverse
реализует алгоритм поиска и разрешения зависимостей ASDF. Сам по себе он не стоит отдельного рассмотрения, но что интересно, это то, что алгоритм может обрабатывать 3 типа зависимостей:- привычную простую форму (
:depends-on (:cl-ppcre ...)
) - версионированную форму (
:depends-on ((:version :cl-ppcre "1.2.3") ...)
) - зависимость от фичи (
:depends-on ((:feature :x86) ...)
)
Точкой входа в механизм поиска Лисп-систем в операционной системе является функция
find-system
, которая смоделированна на основе стандартной функции find-class
. (Артефактом такого подобия является параметр errorp
, необходимость в котором как здесь, так и в find-class
и find-method
, в которых также реализован этот подход, как по мне, по меньшей мере сомнительна). Find-system
одновременно проверяет наличие системы среди уже загруженных (таблица таких систем со врменем последнего обновления находится в *defined-systems*
), а также ищет на диске с помощью функции system-definition-pathname
, которая в свою очередь раскручивает механиз поиска систем функциями, заданными в списке *system-definition-search-functions*
. На данный момент в этом списке 2 основных функции: "классический" поиск в директориях, заданных в *central-registry*
, и новый поиск в source-registry
. Очень важной особенностью find-system
, про которую не нужно забывать, это ее побочный эффект — загрузка ASD-файла системы в память и регистрация его в *defined-systems*
.Наконец, стоит еще упомянуть 2 обобщенные функции
output-files
и input-files
, которые позволяют задавать способ точного определения полных имен файлов разных типов компонент по их короткому имени в описании системы.В общем-то, это и всё ядро ASDF. Остальное — это ряд утилит для работы с операционной системой, среди которых несколько очень полезных функций, заслуживающих более широкой известности в Лисп-мире (например,
run-shell-command
, load-pathname
, parse-windows-shortcut
и другие), а также добавленные в ASDF 2 механизмы определения местонахождения FASL-файлов (в чем-то аналог ASDF-BINARY-LOCATIONS, для которого добавлен и compatibily mode) и работы с центральным реестром.Разобравшись во внутренностях ASDF я пришел к довольно неожижанному для себя выводу: в ее основе лежит хорошо продуманный объектно-ориентированный дизайн, дающий воможность для ее эффективного применения не только непосредственно, но и как основы для других инструментов. Более того, используя ее как кейс, можно даже учить людей настоящему практическому объектно-ориентированному проектированию2. В то же время, этот дизайн, конечно, не идеален.
- Во-первых, он сложен. И это действительно оправдано причинами, описанными вначале. Но все же временами наблюдается излишняя сложность. (Впрочем, эта проблема постепенно устраняется по мере развития кода). Сложность приводит к багам, некоторые из которых существуют до сих пор, на 10-м году жизни системы.
- Во-вторых, он расширяемый, но тоже не до конца: ядро системы создано с оглядкой на последующую расширяемость, но в поддерживающем слое об этом иногда забывали.
- В-третьих, он плохо описан. И это, пожалуй, самая большая проблема ASDF и хороший урок для любого проектировщика: ясная и полная документация имеет важнейшее значение для удачного использования сколь угодно хорошого дизайна.
И еще можно сказать, что ASDF намного ближе по своей философии к (качественным) продуктам "классических" системных языков, таких как С++ или Java, чем к распространенному в последнее время в Лиспе bottom-up стилю. В то же время, за счет использования полезных возможностей Лиспа: мультиметодов, функций высших порядков и т.п.,— он намного менее церемониален и многословен, так сказать, без перегрузки шаблонами проектирования.
В следующей статье — о некоторых шаблонах использования ASDF.
1 разница в том, что изменение "слабых" зависимостей не вызывает перекомпиляцию зависящих от них компонент
2 ... а не такому далекому на поверку от реальности, который можно увидеть, например, в знаменитой книге Гради Буча
И в придачу, непонятная визуализация того, как происходит поиск системы:)
No comments:
Post a Comment