Библиотека функций к Script-fu
Введение.
Итак, теперь наша система позволяет описывать классы с иерархиями множественного наследования и описывать обобщённые функции(generic function) и они придают динамику, придают жизнь создаваемым в системе объектам. Но так ли хороши описанные нами обобщённые функции? Да с точки зрения широко распространённых("классических") ООП систем, они полностью повторяют функциональность методов объектов. При вызове обобщённой функции, происходит диспетчеризация вызова и выбирается наиболее подходящий по типам аргументов метод обобщённой функции. НО в CLOS это НЕ ТАК!!! Да в простейшем случае это так, НО..! CLOS предоставляет более гибкий способ организации кода, когда выполняемый при вызове обобщённой функции код представляет собой не один метод, а целую группу методов. Причём создаётся эта группа динамически в момент вызова, в зависимости от текущих аргументов обобщённой функции(вернее их типов/классов). А в основе такой организации кода лежит спецификация методов обобщённой функции различными квалификаторами
.
Квалификаторы методов
CLOS выделяет 4 квалификатора методов: before
, primary
, after
и arorund
. Эти квалификаторы определяют поведение и очерёдность их вызова в момент применения обобщённой функции.
Квалификатор |
Роль метода |
нет |
Основной метод(primary) |
:before |
Вызывается перед основным методом |
:after |
Вызывается после основного метода |
:around |
|
Стандартный тип комбинации методов поддерживает call-next-method(вызов следующего метода) в around-методах и в первичных методах.
При применении обобщённой функции происходит следующее:
Вызываются ВСЕ подходящие методы с квалификатором before
, сначала наиболее специфичные, а затем менее специфичные.
Вызывается один наиболее спецефичный первичный метод(может не иметь квалификатора при определении) primary
. Этот метод может вызвать следующий менее специфичный метод с помощью call-next-method
и т.д по цепочке.
Затем вызываются ВСЕ подходящие методы с квалификатором after
, сначала наименее специфичные(самые базовые), а потом всё более и более специфичные.
Это называется стандартной комбинацией методов
.
НО если мы определим around
метод, то он вызовется первее всех. И также вызовется только один наиболее специфичный метод. С помощью функции call-next-method
он может вызвать менее специфичный around
метод ИЛИ вызвать стандартную комбинацию методов
(если она есть).
И всё это делается для того что бы лучше организовать код, уменьшить его дублирование, более полно использовать возможности множественного наследования. Теперь перейдём к вопросу как это реализовать.
Реализация.
Чтобы ввести квалификаторы их прежде всего надо определить. Кроме того поскольку у нас вместо одного метода может вызываться комплекс методов, надо создать структуру сохраняющую этот комплекс, чтобы удобно было по ней проходить и что бы можно было её кешировать, и не выполнять это построение при каждом вызове обобщённой функции.
;;вводим квалификаторы методов
(define-macro (def-keys . param-list) ;;описывает список ключевых слов
`(begin ,@(make-def-keys param-list)))
(struct qualifier-methods (primary before after around))
(define (qm-get stru key)
(case key
(':primary (qualifier-methods-primary stru))
(':before (qualifier-methods-before stru))
(':after (qualifier-methods-after stru))
(':around (qualifier-methods-around stru))
))
(define (qm-set! stru key val)
(case key
(':primary (qualifier-methods-primary! stru val))
(':before (qualifier-methods-before! stru val))
(':after (qualifier-methods-after! stru val))
(':around (qualifier-methods-around! stru val))
))
(def-keys :before :after :around :primary)
(define-m (qualifier? s)
(or (eq? s :before)
(eq? s :after)
(eq? s :around)
(eq? s :primary)))
макрос для определения обобщённой функции.
(define-macro (defgeneric name)
(let* ( ;;(required-params (trim-parameters params))
(params (gensym))
(shablon-call (gensym))
(cache-method (gensym))
(applicable-method (gensym))
(name-modify (make-symbol name "-modify-method"))
(name-get-methods (make-symbol name "-get-methods")) ;;for debug
(name-get-methods-all (make-symbol name "-get-methods-all"))
(name-get-cache-methods (make-symbol name "-get-cache-methods")) ;;for debug
(func (gensym))
(fnd-method (gensym))
(qual (gensym)))
`(begin
(define ,name)
(define ,name-modify)
(define ,name-get-methods)
(define ,name-get-methods-all)
(define ,name-get-cache-methods)
(let ((*methods-cache* (make-hash 32))
(*methods* (qualifier-methods! '() '() '() '())))
(set! ,name (lambda-m ,params
(let* ((,shablon-call (apply make-shablon-call-by-args ,params))
(,applicable-method (,name-get-methods ,shablon-call)))
(if (and (car ,applicable-method)
(has-run-method (cdr ,applicable-method)))
(begin
(apply call-methods (cdr ,applicable-method) ,params))
(error (join-to-str "Can't find applicable method: " ',name ", params: ") ,params "\n")))
))
(set! ,name-modify (lambda-m (,shablon-call ,func ,qual)
(when (> (hash-table-size *methods-cache*) 0)
(set! *methods-cache* (make-hash 32))) ;;просто сбросим кеш таблицу.
(let ((,fnd-method (find-method (qm-get *methods* ,qual) ,shablon-call)))
(if (cdr ,fnd-method)
(set-cdr! (car ,fnd-method) (cons ,func '()))
(qm-set! *methods* ,qual (cons (list ,shablon-call ,func) (qm-get *methods* ,qual)))))))
(set! ,name-get-methods
(lambda-m (,shablon-call)
(let ((,cache-method (hash-ref *methods-cache* ,shablon-call)))
(if (car ,cache-method)
,cache-method
(let ((,applicable-method (build-applicable-methods *methods* ,shablon-call)))
(if (has-run-method ,applicable-method)
(begin
(hash-set! *methods-cache* ,shablon-call ,applicable-method)
(cons #t ,applicable-method))
(cons #f '()))
)))))
(set! ,name-get-methods-all (lambda ()
*methods*))
(set! ,name-get-cache-methods (lambda ()
*methods-cache*))
))))
(define (find-method methods shablon)
(find (lambda (x)
(equal? shablon (car x))) methods))
В макрос определения обобщённой функции мы внесли создание функции возвращающей набор методов для конкретного шаблона вызова ,name-get-methods
. Для работы она не особо нужна, т.к действия по работе с хешем выполняются в основной обобщённой функции ,name
(вместо ,name подставляется имя обобщённой функции), но может пригодиться при отладке работы системы. Ещё функция модификации методов стала принимать параметр qual - квалификатор, теперь все методы обобщённой фукнции разделены по квалификаторам, т.е разным спискам, для каждого квалификатора он отдельный.
макрос определения метода
;;будем в начале старта определять квалификатор метода, если его нет, то метод является primary-первичным.
(define-macro (defmethod start . body)
(let* ((have-qual (qualifier? (car start)))
(name (if have-qual (cadr start) (car start)))
(params (if have-qual (cddr start) (cdr start)))
(qual (if have-qual (car start) :primary))
(names-params (map (lambda (x) (if (list? x)
(car x)
x))
params))
(name-modify (make-symbol name "-modify-method"))
(shablon-call (make-shablon-call-by-params params))
(chains-methods (gensym))
(tmp-cur (gensym))
(in-primary (gensym)))
`(begin
(when (not (defined? ',name)) ;;больше нет необходимости вызвать defgeneric
(defgeneric ,name))
(,name-modify ',shablon-call
;;тело метода модифицируем для возможности использовать call-next-method
,(cond
((eq? qual :primary)
`(lambda-m ,(cons chains-methods names-params)
(let* ((next-method-p (lambda () (not (null? (qm-get ,chains-methods :primary)))))
(call-next-method (lambda ()
(call-methods-rec ,chains-methods ,@names-params))))
,@body))
)
((eq? qual :around)
`(lambda-m ,(cons chains-methods names-params)
(let* ((next-method-p (lambda ()
(or (not (null? (qm-get ,chains-methods :before)))
(not (null? (qm-get ,chains-methods :after)))
(not (null? (qm-get ,chains-methods :primary)))
(not (null? (qm-get ,chains-methods :around))))))
(call-next-method (lambda ()
(call-methods-rec ,chains-methods ,@names-params))))
,@body)))
(#t
`(lambda-m ,names-params
,@body)
))
,qual))
))
Макрос определения метода обобщённой функции стал немного сложнее, в нё появилась обработка квалификаторов и в зависимости от квалификаторов в окружение создаваемой функции вносятся локально определяемые функции call-next-method
и next-method-p
. Именно они вызовуться когда вы в коде метода укажете проверку или вызов следующего по иерерахии метода. Но сама функция call-next-method
является лишь обёрткой для функции call-methods-rec
в которой и происходит вызов всего имеющегося комплекса методов, применимых для текущего набора типов аргументов.
Создание шаблонов типов параметров, шаблонов типов текущих аргументов и их предков совершенно не изменилось(и здесь не приводится). А функция filter-acceptable-methods
фильтрации применимых методов и функции find-applicable-method
, find-extreme
потеряли свою актуальность. Зато на первый план выходит функция построения комплекса применимых методов build-applicable-methods
.
(define (build-applicable-methods methods shablon)
(let* ((shablon-parents (build-shablon-parents shablon))
(acceptable-methods-primary (build-acceptable-method-list (qualifier-methods-primary methods) shablon-parents))
(acceptable-methods-before (build-acceptable-method-list (qualifier-methods-before methods) shablon-parents))
(acceptable-methods-after (build-acceptable-method-list (qualifier-methods-after methods) shablon-parents))
(acceptable-methods-around (build-acceptable-method-list (qualifier-methods-around methods) shablon-parents))
(compare-func (make-compare-shablon-call shablon-parents))
(rez (qualifier-methods! '() '() '() '())))
(qualifier-methods-around! rez (sort-c compare-func acceptable-methods-around))
(qualifier-methods-before! rez (sort-c compare-func acceptable-methods-before))
(qualifier-methods-primary! rez (sort-c compare-func acceptable-methods-primary))
(qualifier-methods-after! rez (reverse (sort-c compare-func acceptable-methods-after)))
rez))
которая использует ранее уже описанные функции построения списка применимых методов build-acceptable-method-list
(которая теперь применяется к различным по квалификаторам методам) и в отличии от предыдущего подхода, где искался один наилучший метод, сейчас происходит сортировка всех применимых методов, с помощью всё той же функции сравнения шаблонов, которая строиться с помощью функции make-compare-shablon-call
. Конечно это долго и если такую работу надо было бы проводить при каждом вызове обобщённой функции, то проще было бы выкинуть этот проект в корзину и забыть, НО это кешируемая функция и в идеале(если не переопределять методы) она выполняется один раз при первом вызове обобщённой функции.
функции построения применимых методов и создания функции сравнения, для сортировки методов.
;;строит список приемлемых методов, т.е методов которые в принипе подходят под имеющиеся параметры.
(define-m (build-acceptable-method-list methods shablon-parents)
(fold (lambda (prev x)
(let ((shablon-methods (car x))
(exclude-method #f)
(new-shablon '()))
(do ((cur-methods shablon-methods (cdr cur-methods))
(cur-parents shablon-parents (cdr cur-parents)))
((or exclude-method
(null? cur-methods)
(null? cur-parents))
(if exclude-method ;;выход из лямбды
prev
(if (and (null? cur-methods)
(null? cur-parents))
(cons (cons (reverse new-shablon) (cdr x) ) prev)
prev)))
(if (eq? (car cur-methods) :unspec) ;;тек аргумент в методе это класс!!
(set! new-shablon (cons :unspec new-shablon))
(if (eq? (car cur-parents) :unspec) ;;тек аргумент в вызове не имеет класса
(set! exclude-method #t) ;;тогда метод не подходит!
(let ((find-in-parents
(find (lambda (v) (eq? (car v) (car cur-methods)))
(car cur-parents))))
(if (cdr find-in-parents);;что то нашли в предках класс аргумента вызова
(set! new-shablon (cons (car find-in-parents) new-shablon))
(set! exclude-method #t)))))))) ;;тогда метод не подходит
'()
methods)
)
(define-m (make-compare-shablon-call shablon-parents)
(lambda (cur-best pretendent)
(let ((pretendent-the-best #f)
(current-the-best #f))
(do ((f (car cur-best) (cdr f))
(s (car pretendent) (cdr s))
(cur-shablon shablon-parents (cdr cur-shablon)))
((or current-the-best
pretendent-the-best
(null? f)
(null? s))
(if pretendent-the-best
#f
#t))
(cond ((and
(eq? (car s) :unspec)
(eq? (car f) :unspec))
#f)
((eq? (car s) (car f))
#f)
((eq? (car s) :unspec)
(set! current-the-best #t))
((eq? (car f) :unspec)
(set! pretendent-the-best #t))
((> (cdr (car s)) (cdr (car f)))
(set! current-the-best #t))
((< (cdr (car s)) (cdr (car f)))
(set! pretendent-the-best #t))
(#t ;;= (cdr (car s)) (cdr (car f)) равенство по уровню,
(let ((first (find (lambda (x) (or (eq? (car x) (car (car f))) (eq? (car x) (car (car s)))))
(car cur-shablon))))
(when (cdr first)
(if (eq? (car (car first)) (car (car s))) ;;касс перетендента первый в списке наследования?
(set! pretendent-the-best #t)
(set! current-the-best #t)))
)))
))))
А теперь опишем функции исполняющие полученный комплекс методов.
(define (has-run-method qm)
(or (not (null? (qualifier-methods-around qm)))
(not (null? (qualifier-methods-before qm)))
(not (null? (qualifier-methods-primary qm)))
(not (null? (qualifier-methods-after qm)))))
(define (call-methods methods . params)
(let* ((qm (qualifier-methods! (qualifier-methods-primary methods)
(qualifier-methods-before methods)
(qualifier-methods-after methods)
(qualifier-methods-around methods))))
(apply call-methods-rec (cons qm params))))
;;ЛОГИКА: проверяем если есть методы окружения, вызываем первый из них
;;а он уже позаботиться о вызове всех остальных если это будет нужно
;; если нет такого метода переходим к нормальной обработке
;; сначала вызываем ВСЕ методы ДО, по цепочке
;; затем вызываем наилучший метод ПЕРВИЧНЫЙ, если ему будет нужно он вызовет дополнительные методы ПЕРВИЧНЫЕ
;; в конце вызываеме ВСЕ методы ПОСЛЕ
(define-m (call-methods-rec qm . params)
(let ((rez #f))
(if (not (null? (qualifier-methods-around qm))) ;; если есть around методы то вызываем только их, пока они не кончатся через call-next-method
(let ((tmp-cur (qualifier-methods-around qm)))
(qualifier-methods-around! qm (cdr tmp-cur))
(set! rez (apply (cadr (car tmp-cur)) (cons qm params))))
(let ((tmp-cur (qualifier-methods-primary qm)))
(unless (null? (qualifier-methods-before qm))
(for-list (el (qualifier-methods-before qm))
(apply (cadr el) params))
(qualifier-methods-before! qm '())) ;;мы исполнили всю цепочку методов before, цепочку обнуляем.
(when (not (null? tmp-cur))
(qualifier-methods-primary! qm (cdr tmp-cur))
(set! rez (apply (cadr (car tmp-cur)) (cons qm params))))
(unless (null? (qualifier-methods-after qm))
(for-list (el (qualifier-methods-after qm))
;;(prn "Call after: " (cadr el) "\n")
(apply (cadr el) params))
(qualifier-methods-after! qm '())) ;;мы исполнили всю цепочку методов after, цепочку обнуляем.
))
rez))
Назначение функции call-methods
создать копию комплекса методов, т.к в процессе исполнения мы планируем менять эту структуру, а она у нас сохраняется в кеше, и если этого не сделать последующие вызовы этого комплекса будут неверными. И запустить обработку комплекса с помощью функции call-methods-rec
. Она и отвечает за правильную последовательность вызова всего комплекса методов обобщённой функции. Эта функция активно и рекурсивно взаимодействует с функцией call-next-method
, так что цепочка вызовов происходящая в ней может быть сложнее чем может показаться на первый взгляд.
И в принципе на этом ВСЁ!! Теперь можно посмотреть что у нас получилось.
Тестовый пример.
подготовка к работе, комманды которые надо дать в консоли Script-fu GIMP для загрузки библиотек.
;;(define path-home "D:")
(define path-home (getenv "HOME"))
(define path-lib (string-append path-home "/work/gimp/lib/"))
(define path-work (string-append path-home "/work/gimp/"))
(load (string-append path-lib "util.scm"))
(load (string-append path-lib "defun.scm"))
(load (string-append path-lib "struct2.scm"))
(load (string-append path-lib "storage.scm"))
(load (string-append path-lib "cyclic.scm"))
(load (string-append path-lib "hashtable3.scm")) ;;хеш который может работать с объектами в качестве ключей!!!
(load (string-append path-lib "sort2.scm"))
(load (string-append path-lib "tsort.scm"))
;;(load (string-append path-lib "cpl-sbcl.scm")) ;;можно выбрать любую из функций упорядочения иерархии классов.
(load (string-append path-lib "cpl-mro.scm"))
;;(load (string-append path-lib "cpl-topext.scm"))
(load (string-append path-lib "struct2ext.scm"))
(load (string-append path-lib "queue.scm"))
(load (string-append path-lib "obj4.scm"))
В качестве примера приведу код из книги Пола Грэма "ANSI Common Lisp" стр.196. В нём используются различные типы методов и after
, и before
, и around
.
(defclass speaker () ())
(defmethod (speak (s speaker) str)
(prn str))
(speak (make-speaker) "I`m hungry")
;;I`m hungry
(defclass intellectual (speaker) ())
(defmethod (:before speak (i intellectual) string)
(prn "Perhaps "))
(defmethod (:after speak (i intellectual) string)
(prn " in some sense"))
(speak (make-speaker) "I`m hungry") ;;I`m hungry
(speak (make-intellectual) "I`m hungry") ;;Perhaps I`m hungry in some sense
(defmethod (:before speak (s speaker) string)
(prn "I think "))
(speak (make-speaker) "I`m hungry") ;;I think I`m hungry
(speak (make-intellectual) "I`m hungry") ;;Perhaps I think I`m hungry in some sense
(defclass courtier (speaker) ())
(defmethod (:around speak (c courtier) string)
(prn "Does the King believe that " string "?")
(if (eqv? read-val 'yes)
(if (next-method-p) (call-next-method))
(prn "Indeed, it is a preposterous idea.\n"))
'bow)
;;здесь небольшое отличие у меня нет возможности в Script-fu получать ввод с консоли, поэтом заменю его простыми константами
(define read-val 'yes)
(speak (make-courtier) "kings will last") ;;Does the King believe that kings will last?I think kings will last bow
(define read-val 'no)
(speak (make-courtier) "the world is round")
;;Does the King believe that the world is round? Indeed, it is a preposterous idea.
;;bow
Наблюдаем сто процентное совпадение с кодом из лиспа.
Заключение
Описанная в этой статье функциональность на 90 процентов повторяет функциональность объектной системы CLOS, да здесь нет метаобъектного протокола, нет такого мощного синтаксиса описания объектов и вообще нет переменных класса. Нет выбора комбинаций методов обобщённой функции(это когда изменяют стандартный порядок выполнения методов).
Но нужна ли вообще эта функциональность? Описание инициализаторов полей вполне себе заменимо описанием в ручную создаваемым конструктором. Переменные классов, да иногда нужны и я их введу немного далее, это несложно. А комбинаторы методов это настолько странное явление, что нигде больше неиспользуется и вполне возможно было излишним при проектировании CLOS.
Вот пример использования комбинатора методов из книги Пола Грэма.
(defgeneric price (x)
(:method-combination +))
(defclass jacket () ())
(defclass trousers () ())
(defclass suit (jacket trousers) ())
(defmethod price + ((jk jacket)) 350)
(defmethod price + ((tr trousers)) 200)
(price (make-instance ’suit))
>550
;; допустимые комбинаторы
;;+ and append list max min nconc or progn
Из кода видно, что вместо использования композиции программист пытается решить проблему наследованием. Костюм наследуется от жакета и брюк. После чего цена получается через сложение в комбинации методов. На мой взгляд это просто неверное проектное решение.
Вот как я переписал вышеприведённый пример через композицию.
(defgeneric price x)
(defclass priced () (price))
(defclass jacket (priced) ())
(defclass trousers (priced) ())
(defclass suit ()
(jacket trousers))
(defmethod (price (p priced)) (vfield p :price))
;;(defmethod (price (jk jacket)) (vfield jk :price))
;;(defmethod (price (tr trousers)) (vfield tr :price))
(defmethod (price (s suit))
(with-slots ((jacket trousers) s)
(+ (price jacket) (price trousers))))
(define (suit! jk tr)
(make-suit :jacket (make-jacket :price jk) :trousers (make-trousers :price tr)))
(price (suit! 350 200))
;;550
Элементы в описании класса suit
переместились из списка наследования в список полей. Появился метод обрабатывающий цену для костюма. Также я написал функцию-конструктор инициализирующую поля. Таким образом если когда нибудь, я встречусь с необходимостью введения комбинаторов методов, я возможно пересмотрю свои взгляды, а пока это решение меня вполне удовлетворяет.