October 17th, 2011

Устройство ASDF. Подсистема загрузки. Подсистема планирования операций. DO-TRAVERSE.

(сказаное ниже относится к версии 2.019.2)

Это 8-ая статья цикла.
Первая статья.
Архитектура ASDF.
Предыдущая статья.

    Ф-ия DO-TRAVERSE необходима для создания древообразного списка (элементы которого списки и вектора). Этот список затем преобразовывается в список правил компиляции и загрузки, с помощью FLATTEN-TREE. В нём, векторами помечены элементы внутрь которых нужно "зайти", взять список (в векторе должен быть только один элемент и являться списком) и "вытащить наружу" элементы этого списка. В описании ф-ии TRAVERSE приводилась схема вызовов используемых ф-ий, вот часть этой схемы имеющая отношений к DO-TRAVERSE:

do-traverse -> do-dep -> resolve-dependency-spec -> resolve-dependency-name
                                     -> do-one-dep -> make-sub-operation
                                                               -> do-traverse <-
                      -> component-visited-p
                      -> component-visiting-p
                      -> component-depends-on
                      -> do-traverse <-
                      -> operation-done-p
                      -> visit-component

    Там же был приведён пример работы ф-ии, для упрощение восприятия дальнейшей информации приведу его и здесь:

Вызов:

  (while-collecting (collect)
      (let ((*visit-count* 0))
         (do-traverse (make-instance 'load-op) (find-system :exp-system) #'collect)
)
)

  ... возвращает (показана только часть):

(#((#((#(NIL)
          (#<COMPILE-OP NIL {B1CE711}>
           . #<FILE-MY "exp-system" "src" "file1">
)

          #(NIL)
          (#<LOAD-OP NIL {B209AC9}>
           . #<FILE-MY "exp-system" "src" "file1">
)

       ...
)
)
)
)
)


Подробное описание логики работы:

1. Определяются локальные переменные *FORCING* и FLAG. Причём *FORCING* получает значение *FORCING* из внешнего динамического контекста, таким образом она (*FORCING* из внешнего контекста) защищается от изменений:

  (let ((*forcing* *forcing*)
          (flag nil)
)

      ...
)


2. Определяются две локальные ф-ии UPDATE-FLAG и DEP.

    2.1 UPDATE-FLAG - гарантирует что в локальной FLAG будет истина или ложь:

       (labels
           ((update-flag (x)
               (orf flag x)
)

         ...
)

      ...
)


        ORF определяется с помощью, весьма полезного, стандартного макроса DEFINE-MODIFY-MACRO:

           (define-modify-macro orf (&rest args)
                or "or a flag"
)


        ... этот макрос определяет другой макрос, который сохраняет в первом аргументе вызываемой ф-ии (в данном случае or) результаты применения ф-ии к этому аргументу и, возможно, другим параметрам.

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
Семантика define-modify-macro в CLHS определяется так:

        define-modify-macro name lambda-list function [documentation] => name

        Например мы можем определить макрос appendf, который соединенные списки будет сохранять в своём первом аргументе:

(define-modify-macro appendf (&rest args)
     append "Save concatenated lists"
)

=> APPENDF

(setq lst '(1 2 3))
=> (1 2 3)

(appendf lst '(4 5 6) '(7 8 9))
=> (1 2 3 4 5 6 7 8 9)

lst
=> (1 2 3 4 5 6 7 8 9)

        В случае, с определением макроса ORF, происходит примерно тоже самое - он будет сохранять в своём первом аргументе результат применения or ко всем переданным аргументам. Без DEFINE-MODIFY-MACRO мы могли бы макрос ORF определить так:

       (defmacro orf (&rest args)
           `(setf ,(car args) (or ,@args))
)

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

    2.2 DEP:

        (dep (op comp)
           (update-flag (do-dep operation c collect op comp)))

        Ф-ия обновляет переменную FLAG на результат вызова ф-ии DO-DEP. Смысл вызова в том, чтобы создать дополнительную операцию OP и активизировать создание соответствующего "плана" для обработки этой операцией зависимых компонент (за исключением операции 'feature, которая проверяет наличие COMP в системной переменной *features*).

3. Проверить, была ли уже обработана пара операция-компонент и если была, то возвратить (по сути) из DO-TRAVERSE порядковый номер произведённой обработки. Проверка осуществляется ф-ией COMPONENT-VISITED-P. Она проверяет есть ли в хэш-таблице, связанной со слотом VISITED-NODES объекта OPERATION (а точнее самого старшего объекта OPERATION в иерархии операций, организующейся по слоту PARENT), запись с ключом (<класс_операции> <компонент>). Значение в этой записи, если она конечно есть, выглядит так: (t <номер_обработки>). <номер_обработки> это номер, который был вычислен инкриментированием счётчика *VISIT-COUNT* и сохранён в конце очередного вызова (DO-TRAVERSE ...).

    (aif (component-visited-p operation c)
   
       (progn
    
          (update-flag (cdr it))
  
            (return-from do-traverse flag)))

4. Далее, проверяется происходит ли уже обработка пары операция-компонент. Проверка делается с помощью ф-ии COMPONENT-VISITING-P, которая проверяет есть ли в хэш-таблице, связанной со слотом VISITING-NODES объекта OPERATION, запись с ключом (<класс_операции> <компонент>). Значение в записи = T, если обработка пары операция-компонент уже происходит. В этом случае сигнализируется ошибка циклической зависимости:

   (when (component-visiting-p operation c)
      (error 'circular-dependency :components (list c))
)


5. Сделать пометку о том, что обработка пары операция-компонент уже происходит - сохранить в хэш-таблице, находящейся в слоте VISITING-NODES объекта OPERATION, запись с ключом (<класс_операции> <компонент>) и значением T:

      (setf (visiting-component operation c) t)

6. Основную часть работы выполнить с гарантией того, что в объекте OPERATION будет сохранена пометка о том, что процесс обработки пары операция-компонент завершён:

   (unwind-protect
     (progn ...)
     (setf (visiting-component operation c) nil)
)


    6.1 Специальная переменная *FORCING* возможно получает значение T: если при создании операции был задан ключ :FORCE (так будет, если этот ключ использовался при вызовах LOAD-SYSTEM, OOS, или OPERATE) с не NIL значением и если:

        - это значение не список.
        - если же это список, то *FORCING* получает значение T если обрабатываемый компонент является системой и при этом имя системы присутствует в этом списке.

    6.2 Далее, в локальном контексте в котором *FORCING* = NIL (это необходимо, чтобы её действие не распространялось на обработку компонентов от которых зависит компонент, обработка которого происходит) циклически перебираются списки возвращенные ф-ией COMPONENT-DEPENDS-ON, на каждой итерации разбираются на голову REQUIRED-OP и хвост DEPS и с ними вызывается локальная ф-ия DEP:

       (loop
          :for (required-op . deps) :in (component-depends-on operation c)
          :do (dep required-op deps)
)


        COMPONENT-DEPENDS-ON берёт из слота IN-ORDER-TO компонента список ассоциированый с переданной операций и после некоторой обработки (подробности см. в конце статьи) возвращает его. В списке слота IN-ORDER-TO описываются правила переопределения порядка загрузки зависимых компонентов. На каждой итерации происходит анализ пары из операции REQUIRED-OP и компонентов DEPS, от которых зависит компонент, переданный в аргументе "c", Для этого создаётся объект операции класса REQUIRED-OP со слотом parent равным текущей операции operation. Если очень грубо охарактеризовать последовательность передач управление, то получится примерно такая цепочка вызовов:

        DEP -> DO-DEP -> DO-ONE-DEP -> DO-TRAVERSE (т.е. неявный рекурсивный вызов)

    6.3 Определение локальной MODULE-OPS. Она получает значение отличное от NIL, если компонент в аргументе "c" является объектом класса MODULE. Далее, идёт процесс формирования значения для MODULE-OPS:

        6.3.1 Определение локальных AT-LEAST-ONE, ERROR и *FORCING*. *FORCING* = T, если одноименная переменная из внешнего контексте = T или если выполняются два условия:
            - анализируемый компонент - не система.
            - FLAG приобрёл значение T в процессе анализа операций над зависимыми компонентами, а это происходит в локальной ф-ии DEP, в случае если хотя бы для одного из них была запланирована операция (один из вызовов DO-DEP в локальной DEP вернул значение отличное от NIL).

        6.3.2 Определяется локальная область с ф-ией INTERNAL-COLLECT, собирающей значения в результирующий список.

            6.3.2.1 Далее, итерация по списку компонентов, возвращенному ф-ией MODULE-COMPONENTS:

               (while-collecting (internal-collect)
                  (dolist (kid (module-components c))
               ...
)
)


                На каждой итерации, устанавливается обработчик ошибки, вызывается DO-TRAVERSE с компонентом и текущей операцией, а FLAG становится эквивалентным T, если один из вызовов вернул значение отличное от NIL:

               (handler-case
                  (update-flag
                    (do-traverse operation kid #'internal-collect)
)

                 ...
)


                Обработчик ошибок перехватывает CONDITION типа MISSING-DEPENDENCY и действует в зависимости от устанавленного в слоте IF-COMPONENT-DEP-FAILS модуля "с" (компонент в данном случае будет объектом класса или подкласса MODULE) значения - если :FAIL (по умолчанию), то ошибка сигнализируется заново. Если обработка хотя бы одного компонента (содержащимся в модуле) была успешной то переменная AT-LEAST-ONE получает значение T:

               (handler-case
                    ...
                   (missing-dependency (condition)
                      (when (eq (module-if-component-dep-fails c)
                                         :fail
)

                        (error condition)
)

                     (setf error condition)
)


                  (:no-error (c)
                     (declare (ignore c))
                  (setf at-least-one t)
)
)

            6.3.2.2 Далее, если ни одна обработка компонента не была успешной и в слоте IF-COMPONENT-DEP-FAILS модуля содержится значение :TRY-NEXT то ошибка сигнализируется заново:

               (when (and (eq (module-if-component-dep-fails c)
                                            :try-next
)

                    (not at-least-one)
)

                    (error error)
)


    6.4 В контексте определённой MODULE-OPS происходит обновление флага и окончательный сбор значений, с помощью переданной ф-ии COLLECT:

        6.4.1 FLAG получает значение равное T, если в OPERATE (а также в LOAD-SYSTEM или OOS) был передан ключ :FORCE T (в этом случае динамическая переменная *FORCING* будет равна T) или если операция не была произведена над компонентом (например, после последней компиляции исходный файл был изменён):

           (update-flag (or *forcing* (not (operation-done-p operation c))))

        6.4.2 Если FLAG = T, значит необходимо запланировать операцию с компонентом.

            6.4.2.1 Планирование и сбор запланированных операций делегируется ф-ии DO-DEP. Которая последовательно получает те операции, которые нужно произвести над компонентами, от которых зависит данный (эта информация содержатся в слоте DO-FIRST):

               (let ((do-first (cdr (assoc (class-name (class-of operation))
                                                            (component-do-first c)
)
)
)
)

                  (loop :for (required-op . deps) :in do-first
                     :do (do-dep operation c collect required-op deps)
)
)


            6.4.2.2 Собирается в итоговый список план операций над содержимым компонента:

                (do-collect collect (vector module-ops))

            6.4.2.3 Планируется операция с самим компонентом:

                (do-collect collect (cons operation c))

7. Если для компонента всё-таки была запланирована операция, FLAG = T (например, если исходный файл был изменён и требуется перекомпиляция) то хэш-таблица в слоте VISITED-NODES самого старого предка операции (который ищется с помощью последовательного чтения слотов parent в объекте операции) модифицируется соответствующим образом - устанавливается значение (t . <номер_обработки>), связанное с ключом '(<имя_класса_операции> . <компонент>). Таким образом устанавливается, необходима ли операция с компонентом. Это влияет на обработку компонентов зависящих от данного - если операция требуется, то значит необходимо запланировать операцию и для зависимого компонента. Если операция не была запланирована, то в упомянутой хэш-таблице сохраняется значение '(t):

   (visit-component operation c (when flag (incf *visit-count*)))

8. Возвращается FLAG (который, как сказано выше, равен T в случае если для компонента была запланирована операция).

----------------
COMPONENT-DEPENDS-ON.

    Это обобщённая ф-ия имеющая в своём составе 5 методов (один из которых декорирующий метод со стандартным комбинатором :around). Ф-ия берёт значение из слота IN-ORDER-TO компонента и корректирует его.
Рассмотрим методы этой ф-ии в порядке увеличения сложности:

(defmethod component-depends-on :around ((o test-op) (c system)) ...)
Добавляет к результату вызова список (load-op <имя_компонента>).

(defmethod component-depends-on ((op-spec symbol) (c component)) ...)
Создаёт объект операции, представляемый символом, и вызывает с ним следующий метод.

(defmethod component-depends-on ((operation load-op) (c component)) ...)
Добавляет список (compile-op <имя_компонента>) к результату вызова следующего, менее специфичного вызова (по умолчанию им будет метод специализирующийся на аргументах (OPERATION COMPONENT), то есть наиболее общий).

(defmethod component-depends-on ((o operation) (c component))
Наиболее общий метод (по умолчанию). Возвращает список элементов ассоциированных с типом операции из слота IN-ORDER-TO компонента:

    (cdr (assoc (type-of o) (component-in-order-to c)))
)


(defmethod component-depends-on ((o load-source-op) (c component))
Возвращает обработанный результат вызова (component-depends-on 'load-op c).
Обработка заключается в выборке списков с первым элементом 'load-op,
заменой в этих списках 'load-op на 'load-source-op и создание из них
результирующего списка:

      (loop :with what-would-load-op-do = (component-depends-on 'load-op c)
          :for (op . co) :in what-would-load-op-do
          :when (eq op 'load-op) :collect (cons 'load-source-op co)
)

----------------------------------------

Продолжение следует ...

Устройство ASDF. Подсистема загрузки. Подсистема планирования операций. TRAVERSE.

(сказаное ниже относится к версии 2.019.2)

Это 7-ая статья цикла.
Первая статья.
Архитектура ASDF.
Предыдущая статья.

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

   (traverse (make-instance 'load-op) (find-system :exp-system))

А вот часть результата:

((#<COMPILE-OP NIL {B898FC1}> . #<CL-SOURCE-FILE "exp-system" "src" "file1">)
 (#<LOAD-OP NIL {B929AB1}> . #<CL-SOURCE-FILE "exp-system" "src" "file1">)
 (#<COMPILE-OP NIL {B898FC1}>
           . #<CL-SOURCE-FILE "exp-system" "src" "file2">
)

 ...
)

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

;;;;;;;;;;;;;;;;;;;
traverse -> do-traverse -> do-dep -> resolve-dependency-spec -> resolve-dependency-name
                                                        -> do-one-dep -> make-sub-operation
                                                                                 -> do-traverse <-
                                       -> component-visited-p
                                       -> component-visiting-p
                                       -> component-depends-on
                                       -> do-traverse <-
                                       -> operation-done-p
                                       -> visit-component
               -> flatten-tree
;;;;;;;;;;;;;;;;;;;    

Разберём по шагам логику работы ф-ии traverse:

1. Если в слоте forced объекта список, то преобразовать элементы списка в имена:

   (when (consp (operation-forced operation))
      (setf (operation-forced operation)
              (mapcar #'coerce-name (operation-forced operation))
)
)


    Значение forced задаётся ключом :forced при вызове operate (а также LOAD-SYSTEM и OOS). Если ключ был задан и равен значению T или списку содержащему, в том числе, имя загружаемой системы, то происходит повторное планирование операций для компонентов системы.

2. Создать динамический контекст со специальной переменной-счётчиком *VISIT-COUNT*, равным 0, определить ф-ию collect для сбора значений:

    (while-collecting (collect)
   
   (let ((*visit-count* 0))
   
      ...))

3. Далее вызывается ф-ия DO-TRAVERSE для формирования результата (окончательная обработка которого будет производится ф-ией FLATTEN-TREE). Ф-ия DO-TRAVERSE возвращает древообразный список (элементы которого списки и вектора), который затем можно преобразовать в список правил компиляции и загрузки с помощью FLATTEN-TREE. В этом древообразном списке, векторами помечены элементы внутрь которых нужно "зайти", взять список (в векторе должен быть только один элемент и являться списком) и "вытащить наружу" элементы этого списка. Форма вызова:

   (while-collecting (collect)
      (let ((*visit-count* 0))
          (do-traverse (make-instance 'load-op) (find-system :exp-system) #'collect)
)
)


    ... вернёт (показана только часть):

(#((#((#(NIL)
          (#<COMPILE-OP NIL {B1CE711}>
           . #<FILE-MY "exp-system" "src" "file1">
)

          #(NIL)
          (#<LOAD-OP NIL {B209AC9}>
           . #<FILE-MY "exp-system" "src" "file1">
)

       ...
)
)
)
)
)

 
    Эта списочная структура создана специально для дальнейшей обработки ф-ией FLATTEN-TREE.

4. Ф-ия FLATTEN-TREE является хорошим примером использования взаимной рекурсии. Логика её работы в следующем:
        - она проходит по списку, собирая элементы в новый список, оставляя все элементы как есть, кроме векторов.
        - для них делается исключение - берётся первый элемент (который должен быть единственным и должен быть списком) и в новый список попадают уже элементы этого списка, а не сам вектор, например:

       (setq l '(1 (2 3) #((4 (5))) 6 #(((7 8)))))
        => (1 (2 3) #((4 (5))) 6 #(((7 8))))

        (flatten-tree l)
        => (1 (2 3) 4 (5) 6 (7 8))

    Для совершенствования навыков чтения кода, использующего взаимно-рекурсивные вызовы, полезно будет проанализировать её определение:

(defun* flatten-tree (l)
  (while-collecting (c)
    (labels ((r (x)
               (if (typep x '(simple-vector 1))
                   (r* (svref x 0))
                   (c x)
)
)

             (r* (l)
               (dolist (x l) (r x))
)
)

      (r* l)
)
)
)

--------------------------------------------------
Продолжение следует ...

Устройство ASDF. Подсистема загрузки. Подсистема планирования операций. DO-DEP.

(сказаное ниже относится к версии 2.019.2)

Это 9-ая статья цикла.
Первая статья.
Архитектура ASDF.
Предыдущая статья.

(defun* do-dep (op c collect dep-op-spec dep-c-specs) ...)

    Приблизительная схема работы состоит в следующем: осуществляется проход по компонентам DEP-C-SPECS, каждый из которых надо проанализировать вместе с операцией, имя которой передаётся через аргумент DEP-C-SPECS. Для каждой пары имя_операции-компонент начинается обработка (предварительно создаётся объект класса операции, представленый именем DEP-C-SPECS со слотом PARENT равным OP) с помощью неявного рекурсивного вызова DO-TRAVERSE (исключение составляет операция 'feature, проверяющая наличие особенности/возможности в системной переменной *FEATURES*).

    Подробная логика работы:

1. Если OP = 'feature, то значит нужно проверить наличие первого элемента в списке DEP-C-SPECS в особенностях системы (в системной переменной *FEATURES*). Если особенность (фича) есть, то просто вернуть NIL, иначе сигнализировать ошибку MISSING-DEPENDENCY. Это необходимо для того, чтобы не создавать "полуработающие" системы. Если система не расчитана например в работе на windows, лучше сигнализировать об этом сразу, чем заставлять пользователя системы заниматься поисками трудноуловимых ошибок. Использовать этот функционал можно определив в значении ключа IN-ORDER-TO операцию FEATURE, например:

   (defsystem :exp-system
      ...
      :in-order-to ((load-op (feature :unix)
                                        (feature :sbcl)
)
)

      ...
)


    ... теперь при загрузке системы, а конкретно при применения операции LOAD-OP к компоненту "exp-system" (класс component один из предков класса SYSTEM) будут проверяться ключи :unix и :sbcl в системной *FEATURES* и при их отсутствии сигнализироваться ошибка MISSING-DEPENDENCY.

2. Создать локальный контекст с FLAG = NIL и организовать итерацию по списку, переданному через аргумент DEP-C-SPECS:

   (dolist (d dep-c-specs) ...)

        На каждой итерации ищется компонент D, а ф-ии DO-ONE-DEP делегируется обработка операции DEP-OP-SPEC с найденным компонентом и анализируется результат:

            (when (do-one-dep op c collect dep-op-spec
                                     (resolve-dependency-spec c d)
)

               (setf flag t)
)
)

    2.1 С помощью resolve-dependency-spec, найти компонент "d" на том же уровне иерархии где находится компонент "с" (второй аргумент ф-ии DO-DEP):

        (resolve-dependency-spec c d)

        (defun* resolve-dependency-spec (component dep-spec) ...)
        2.1.1 Если аргумент DEP-SPEC - атом (а он скорее всего будет атомом), то значит он представляет имя компонента, который надо найти на том же уровне иерархии компонентов. Поиск производится ф-ией RESOLVE-DEPENDENCY-NAME, которая получает значение слота PARENT компонента и ищет в его содержимом компонент с именем DEP-SPEC (в RESOLVE-DEPENDENCY-NAME - аргумент NAME):

            (find-component (component-parent component) name)

        В ней же (в RESOLVE-DEPENDENCY-NAME) осуществляется сигнализация ошибки отсутствия компонента missing-dependency и проверка версии (и сигнализация соответствующей ошибки MISSING-DEPENDENCY-OF-VERSION). Версия проверяется если, компонент был найден и задан опциональный аргумент VERSION.

        2.1.2 Если аргумент список и его первый элемент ключевой символ :VERSION, то также использовать RESOLVE-DEPENDENCY-NAME, но в качестве опционального аргумента указать требуемую версию:

           (resolve-dependency-name component (second dep-spec) (third dep-spec))

        То есть список должен быть в виде (:version <компонент> <версия>). Ничего не кажется странным? Меня вот сразу заклинило на этой строчке - кажется алогичным хранение версии в третьем элементе переданного списка. Хотя вопрос спорный.

        2.1.3 Если же первый элемент списка :FEATURE, то значит была указана особенность (возможность/фича) ключом :FEATURE. Этот ключ необходим для того, чтобы не обрабатывать компонент, в случае, если указанной особенности нет в системной переменной *FEATURES*.  При использование ключа сигнализируется ошибка SIMPLE-ERROR с установленным стандартным рестартом CONTINUE. Текст ошибки весьма любопытен:

            "Congratulations, you're the first ever user of FEATURE dependencies! Please contact the asdf-devel mailing-list."

            Неожиданный ход. В документации, в соответствующем разделе этот момент тоже упоминается, так что всё в порядке. Там же, подробно описана причина из-за которой имеет место такой нюанс. Суть её в том, что использование *FEATURES* может быть непереносимо между разными лисп-системами. Для единообразного использования *FEATURES* в разных лисп-системах даже создана специальная библиотека TRIVIAL-FEATURES (http://www.cliki.net/trivial-features). Если вам необходимо использовать ключ :features в описании компонентов, то не лишним будет последовать совету разработчиков и написать в почтовую рассылку. От себя добавлю что это вообще является лишним и лучше использовать стандартные макросы чтения вроде #+ и #- .

    2.2 Делегировать ф-ии do-one-dep создание операции DEP-OP-SPEC и вызов ф-ии DO-TRAVERSE.

        DO-ONE-DEP
        (defun* do-one-dep (op c collect dep-op dep-c) ...)
        2.2.1 Создать операцию класса DEP-OP и содержащую операцию OP в своём слоте PARENT:

            (make-sub-operation c op dep-c dep-op)

        2.2.1 Произвести неявный рекурсивный вызов DO-TRAVERSE с созданной операцией и найденным компонентом DEP-C.

    2.3 FLAG станет равным T, если вызов DO-ONE-SPEC на любой из итераций был успешен. Возвратить его в конце цикла:

(defun ...
 
  (cond ...
  
    (t (let ((flag nil))
        
   (dolist ...
           
   (when (do-one-dep operation c collect
                                                op comp ver
)

             
     (setf flag t)))
      
        flag))))

--------------------------------------
Продолжение следует ...

Устройство ASDF. Предисловие.

    В программировании на мэйнстримных языках бытует такое мнение, что система не должна быть чрезмерно гибкой. К такой точке зрения вполне можно отнестись с пониманием - ведь "гибкость" даётся не просто так, система становится сложнее, а значит больше риск внесения ошибок и соответственно это требует больше ресурсов для поддержки, Конечно, этот тезис следует иметь в виду при работе с любыми технологиями программирования. А вот в мире Лиспа дело обстоит несколько иначе: та гибкость которая обычно считается чрезмерной, здесь таковой не является, а недостаточная гибкость, считается скорее моветоном. Почему так? Да просто средства обеспечения гибкости в Лиспе достаточно просты, отлично гармонизируют с другими механизмами языка и друг другом (чего стоят одни только макросы). То есть, использования этих средств не является долгим/трудоёмким процессом (понятное дело, если не доводить это до абсурда). Дело в фундаментальной основе самого языка: минималистичный синтаксис, гомоиконность (программы представляются также как и данные), представление программ и данных в древовидной форме (в виде иерархических списков, в виде графов ... называйте как хотите). Ну а дальше уже из этого следуют/вытекают макросы, лёгкость построение "Embedded DSL" (встроеных языков предметной области) и прочее-прочее ... . Но довольно лирики, в этой статье мы поговорим об инструменте определения систем (если по проще, то: о средстве работы с библиотеками) - ASDF (Another System Definition Facility - другое средство определения систем), а точнее о элементах его внутреннего устройства и его некоторых качествах, обеспечивающих высокую гибкость.

    Это первая статья из планируемого цикла статей по внутреннему устройству ASDF. Она предназначена для знакомых с языком Common Lisp, любителей "полазить" по чужим исходным кодам и интересующихся внутренним устройством ASDF. Следует иметь в виду, что это не какое-либо подробное объяснение всех нюансов внутреннего устройства, но и не концептуальное его изложение, а лишь некий путеводитель для изучение исходного кода. В этом цикле будет рассматриваться последняя на момент написания цикла development версия, хотя практически вся информация будет долгое время актуальна и для последующих версий. Если вам нужно лишь общее представление об устройстве ASDF и рекомендации к использованию, а технические подробности внутреннего устройства вас мало интересуют, я рекомендую прочитать эти статьи:

    http://lisp-univ-etc.blogspot.com/2010/06/asdf-2.html
    http://lisp-univ-etc.blogspot.com/2010/07/asdf.html
    http://lisp-univ-etc.blogspot.com/2010/08/asdf.html

Также они рекомендуются, если вы только начинаете знакомиться с библиотекой ASDF. И конечно же не стоит забывать об официальном руководстве:

    http://common-lisp.net/project/asdf/asdf.html

Крайне рекомендуется подходить к изучения цикла статей с экспериментальной точки зрения:
    1. Узнать где у вас лежит исходник asdf.lisp (желательно чтобы он был такой же версии как будет указано в каждой из статей, на момент написания этих строк, это версия 2.019.2), хотя небольшие различия в минорной версии не должны усложнить понимание материала). Или скачать его, если ASDF не входит в штатную поставку с вашей лисп-системой.
    2. В процессе изучения статей, непосредственно анализировать место в исходном коде о котором идёт речь.
    3. Интенсивно использовать отладочные инструменты вашей лисп-системы и IDE - такие формы как (step ...), (trace ...), (break ...) и Slime debugger, в случае использования SLIME.

    Спасибо за внимание. Надеюсь этот цикл статей для кого-то будет интересен. А кому не понравится, воспринимайте это просто как "фан-арт":)

Продолжение: вторая статья цикла.

---------------------
Ссылки:

Домашняя страничка проекта:

    http://common-lisp.net/project/asdf/

Страничка посвященная ASDF на cliki'ах (там же можно найти ссылки на короткие "туториалы")

    http://www.cliki.net/asdf

Получить самую свежую, разрабатываемую git-версию можно выполнив:

    git clone git://common-lisp.net/projects/asdf/asdf.git

Также можно воспользоваться веб-интерфейсом к git-репозитарию:

    http://common-lisp.net/gitweb?p=projects/asdf/asdf.git

Узнать текущее состояние дел разработки проекта можно по ссылке:

    https://launchpad.net/asdf
------------------------------------------------