Библиотека функций к 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 переместились из списка наследования в список полей. Появился метод обрабатывающий цену для костюма. Также я написал функцию-конструктор инициализирующую поля. Таким образом если когда нибудь, я встречусь с необходимостью введения комбинаторов методов, я возможно пересмотрю свои взгляды, а пока это решение меня вполне удовлетворяет.

Комментарии (0)