Регистрация | Войти
Lisp — программируемый язык программирования

Комбинаторы методов

Автор: Дмитрий Игнатьев

Источник: http://love5an.livejournal.com/350660.html

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

Комбинатор методов это сущность(грубо говоря, код), ответственная за вычисление так называемого "фактического метода"(effective method), который определяет, в каком порядке будут вызываться(и будут ли вызываться вообще) конкретные методы обобщенной функции для данного конкретного набора аргументов, и как комбинировать возвращаемые значения этих методов.

Наиболее часто используемый, и используемый обобщенными функциями по умолчанию, комбинатор методов называется "стандартный комбинатор методов". Стандартный комбинатор методов различает четыре вида последних:

  • Основной метод. То есть, тот самый, который определялся без каких-либо спецификаторов:
    (defmethod foo (...аргументы...) ...тело метода...)
  • Метод, вызываемый непосредственно перед вызовом основного метода. Определяется со спецификатором :before
    (defmethod foo :before (...аргументы...) ...тело...)
  • Метод, вызываемый непосредственно после вызова основного метода. Определяется со спецификатором :after
  • И метод, вызываемый "вместо" основного метода - :around.

Фактический метод, создаваемый стандартным комбинатором методов, вызывает конкретные методы обобщенной функции следующим образом:

  • Сначала вызывается наиболее специфичный :around метод. Значения, которые он возвращает - те самые, которые возвращает собственно обобщенная функция при ее вызове.
  • Если в теле :around присутствует макрос call-next-method, он вызывает следующий наиболее специфичный :around метод
  • Если, при этом, текущий :around метод - наименее специфичный, или же если :around методов для данного набора аргументов не определено, то:
  • Вызываются все :before методы, в порядке от наиболее специфичного к наименее специфичному.
  • Вызывается наиболее специфичный основной метод. Внутри основного метода можно использовать макрос call-next-method, чтобы вызвать следующий наиболее специфичный основной метод.
  • Вызываются все :after методы, в порядке от наименее специфичного к наиболее специфичному(т.е. в обратном :before порядке).

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

:before и :after методы могут быть полезны, к примеру, для логгирования каких-либо действий. Но не только, естественно.
Конкретно я, например, использую их в Doors - определяю :after метод shared-initialize для COM-интерфейсов, которые наследуются от IUnknown, в котором финализирую интерфейс вызовом метода Release (shared-initialize вызывается, cреди прочего, обобщенными функциями initialize-instance и reinitialize-instance, первая из которых, в свою очередь, вызывается в стандартном конструкторе объектов CLOS-классов):

(defmethod shared-initialize :after
  ((object unknown) slot-names &rest initargs &key finalize &allow-other-keys)
  (declare (ignore slot-names initargs))
  (when finalize
    (let ((pobject (com-interface-pointer object)))
      (declare (type pointer pobject))
      (when (&? pobject) ;;Проверка указателя на NULL
       (finalize object (lambda ()
                           ;;Вызывается третий метод интерфейса,
                          ;;то есть IUnknown->Release()
                          (external-pointer-call
                             (deref (&+ (deref pobject '*) 2 '*) '*)
                             ((:stdcall)
                              (ulong)
                              (pointer this :aux pobject)
)
)
)
)
)
)
)

  object
)

Могу привести и пример использования :around метода:

Virgil способен автоматически маршалить циклические структуры данных. Но поддержка этого в нем появилась не сразу.

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

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

(defmethod write-value :around (value pointer (type translatable-type))
  (if (and *handle-cycles* (not (immediate-type-p type)))
    (let ((cell (assoc value *written-values* :test #'eq)))
      (if cell
        (car cell) ;;если объект уже был записан, основной метод
                  ;;write-value уже вызываться не будет
       (progn (push (cons value pointer) *written-values*)
               (call-next-method)
)
)
)

    (call-next-method)
)
)

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

Например, комбинатор progn последовательно вызывает все подходящие методы обобщенной функции, и возвращает значение, возвращенное последним; порядок, в котором они вызываются, может быть указан в опции :method-combination макроса defgeneric - он может быть либо :most-specific-last, либо :most-specific-first.

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

(defclass basic-test ()
  ())


(defclass advanced-test (basic-test)
  ())


(defgeneric test (test)
  (:method-combination and :most-specific-last)
)


(defmethod test and ((test basic-test))
  (write-line "Basic test passed")
  t
)


(defmethod test and ((test advanced-test))
  (write-line "Advanced test passed")
  :ok
)


;; (test (make-instance 'advanced-test))
;; на stdout'е ==> Basic test passed
;;                 Advanced test passed
;; ==> :OK (возвращенное значение)

Кстати, встроенные комбинаторы тоже позволяют определять :around методы.

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

(define-method-combination ordered-method-call
    () ((method-group positive-integer-qualifier-p))
  (let ((ordered-method-list
          (sort method-group #'<
                :key (lambda (method)
                       (first (method-qualifiers method))
)
)
)
)

    `(progn ,@(loop :for method :in ordered-method-list
                :collect `(call-method ,method)
)
)
)
)


(defun positive-integer-qualifier-p (qualifiers)
  (and (= 1 (length qualifiers))
       (typep (first qualifiers) '(integer 1 *))
)
)


(defgeneric foo ()
  (:method-combination ordered-method-call)
)


(defmethod foo 1 ()
  (write-line "This is the first method")
)


(defmethod foo 9000 ()
  (write-line "This is the last method(i hope!)")
  T
)


(defmethod foo 3 ()
  (write-line "This is the third method")
)


(defmethod foo 2 ()
  (write-line "This is the second method")
)


;; (foo)
;; вывод на stdout ==> This is the first method
;;                     This is the second method
;;                     This is the third method
;;                     This is the last method(i hope!)
;; ==> T
@2009-2013 lisper.ru