Search
Write a publication
Pull to refresh

GIMP Script-Fu ООП. Dot синтаксис и другой синтаксический сахар

Level of difficultyMedium
Reading time13 min
Views203

Библиотека функций к Script-fu

Введение

После написания объектной системы для Script‑fu я задумался над примерами, на которых хорошо бы было проверить эту систему. Я прошерстил уйму литературы, но хороших примеров использующих все возможности ООП в литературе встречается крайне мало. Ну что толку реализовывать класс List в Лиспе? А класс Stack? Примерами подобных классов пестрят книжки по Си++. Всё было не интересно, но вот я встретил книжку «Теория вычислений для программистов» Тома Стюарта, и вот примеры из неё, написанные на Ruby, показались мне интересными. Да в Ruby нет множественного наследования, но есть возможность создавать миксины, что несколько сглаживает этот недостаток. И вот делая примеры из этой, безусловно замечательной(с точки зрения теории вычислений) книжки я заметил, что мой код получается более длинный, чем код Ruby. И это происходит не только из за наличия операторов в Руби. Сами определения классов и методов в Руби получаются компактнее, и требуют меньше кода.

Различия в синтаксисе, дающие преимущества Руби(и не только ему) перед Лисп

  • Операторный синтаксис

;;Ruby
a + b

;;lisp и другой фунциональный подход +(a b)
(+ a b)

мелочь, две скобки, но такие мелочи могут накапливаться. Но функциональный подход в данном случае я считаю всё же преимуществом, тем более что на повороте он может и обойти Руби(и др.)

(+ a b c d) ;;и чем круче такой "поворот" тем больше синтаксический выигрышь лиспа.

  • Точечный доступ к атрибутам(полям) объектов. Dot-синтаксис.

;;Руби
obj.field

;;мой способ доступа к слотам объекта
(vfield obj :field)

;; CLOS
(slot-value obj 'slot)

да длинновато, это точка боли, но кое-что я смогу вам предложить.

  • Определения методов и классов

;;Руби
class Variable < Struct.new(:name)
	def to_s
	   name.to_s
	end
	
	def inspect
	   "<#{self}>"
    end

    def reducible?
	   true
	end
end

;;моя ооп, да и CLOS тоже.
(defclass Variable (Object)
   (name))

(defmethod (inspect (o Variable) cycle)
   (to-str (vfield o :name)))

(defmethod (reducible? (o Variable))
  #t)
;;за скобками сравнения  

(defclass Object () ())

(defmethod (to-s (o Object))
  (inspect o nil))

Даже для такого простого, я бы даже сказал пустого, случая и не смотря на то что у меня метод to-s наследуется получается как то многословно. И это мы ещё не описываем обобщённую функцию с помощью defgeneric. Мы постоянно описываем первый параметр метода, тогда как в Ruby этого нет, это всё делается по умолчанию. Там заранее известно что метод работает с объектом класса. Так зачем это второй раз описывать?(как например это делается зачем‑то в Питоне). А не сможем ли мы создать такой синтаксис, который представил бы наши методы, не методами обобщённых функций, а методами принадлежащими классу, и тогда не надо будет постоянно описывать этот первый объект и его тип? Да сможем! СМОЖЕМ! Давайте приступим:

Реализация "ЧАСТНОГО СЛУЧАЯ"

Это один из самых ПРОСТЫХ макросов, который можно написать, он создаёт анафору(захватывает переменную в описываемом коде и позволяет на неё ссылаться в этом коде) с переменной self, обозначающую объект указанного класса и являющийся первым параметром метода.

;;Хочу создать синтаксис позволяющий описывать методы, как будто эти методы принадлежат определённому классу
(defmethods Class
  (Method1 ((p1 TypeParam) (p2 TypeParam) p3 p4 ...)
     ..... self - ссылка на объект типа Class передаваемый в метод первым, по мимо параметров p1 ...pn
   )
  (Method2 (p1 (p2 TypeParam2))
     (with-slots (((f1 field1) field2) self
                  ((f2 field3) field4) p2)
     .......))
)
;; записи превращаются в
;; (defmethod (Method1 (self Class) (p1 TypeParam) (p2 TypeParam) p3 p4 ...) body)
;; (defmethod (Method2 (self Class) p1 (p2 TypeParam2)) body)

(define-macro (defmethods class . methods)
  (let* ((methods-list '()))
    (for-list (el methods)
       (let ((name-method (car  el))
             (params      (cons (list 'self class) (cadr el)))
             (body        (cddr el)))
         (push methods-list
               `(defmethod (,name-method ,@params) ,@body))))
    `(begin
       ,@(reverse methods-list))))

Замечание, данная реализация макроса не позволяет объявлять квалификаторы для методов(но всё исправлено в obj5.scm).

Как теперь будет выглядеть наш код, при использовании этого макроса?

(defclass Variable (Object)
   (name))

;; определяем сразу два метода
(defmethods Variable
  (inspect (cycle)
    (to-str (vfield self :name)))

  (reducible? () #t))
)

Внимательный читатель заметит, что код в лиспе делает что-то немного отличное от кода на Руби, но это всё благодаря классу Object, который я наследую. Именно там определена обобщённая функция и метод to-s, переводящая объект в строку, используя наиболее конкретный(подходящий) текущему типу объекта метод inspect. Хотя на самом деле можно было бы не описывать метод inspect, базовый метод для класса Object всё равно бы обработал любой тип объекта и распечатал значения всех его полей, обработав и взаимно-рекурсивные объекты. Но не в этом суть, суть в том что синтаксис определений методов стал значительно короче и это на смешном примере, представьте как сократиться синтаксис определения 5 или даже 10 методов.

Таким образом программист освоивший две синтаксических конструкции defclass и defmethods не смог бы найти отличия между синтаксисом CLOS и Руби, или например Питоном. Ну абстрагировавшись от незначительных мелочей. Ну в Руби объявление класса, фактически объявляет только методы, а определения полей стоят где то там сбоку, в специальном классе Struct. Но в семантике полученного синтаксиса разницы нет. Вот поля класса(объекта), а в питоне их вообще не объявляют, статические поля класса(из предыдущей статьи) и определения методов, которые наследуются и перегружаются. Ну всё тоже самое.

Но вывод что я могу сделать совершенно неожиданный. Поскольку реализовав подобный синтаксис, я произвёл сужение функциональности обобщённых функций( я как бы отнёс их методы к определённому классу), значит выразительность объектных систем реализующих концепции CLOS гораздо(ну ладно уж - НЕМНОГО) ШИРЕ, преобладающих сейчас на рынке популярных ОО систем. Теперь и у нас "всё как у людей".

Удобный доступ к полям(слотам) объекта

Длинный доступ к полям никому не нравится. В CLOS придумали макрос with-slots позволяющий связать некоторый алиас со слотом поля некоторого объекта. Я разработал аналогичный макрос и для своей системы. В моём случае этот макрос нужен не только для упрощения синтаксиса, но и для ускорения работы методов.

И всё по очень простой причине:

Не смотря на то, объект представляет собой вектор и доступ к полям это быстрая операция, для обобщённого метода мы заранее не знаем, какой объект у нас будет принят в качестве аргумента: объявленного типа параметра, или же какой то из его потомков. А значит надо по актуальному типу и имени поля находить в хеш-таблице метод, для доступа к полю. А хеш-таблица работает с символами крайне медленно и всё "благодаря" неким умникам из команды GIMP, которые закрыли возможность динамической загрузки расширений в Script-fu(можно пересобрать скрипт-фу с этой возможностью, но вряд ли кто будет это делать). А без этих расширений расчёт хеш-кода символа, превращается в расчёт хеш-кода строки, а мог бы вычисляться в мгновенье ока через сишную-функцию, ориентирующуюся на адрес памяти размещения символа. Поэтому я стараюсь избегать чрезмерного злоупотребления хеш-таблицами с ключами символами. Поэтому в нашем макросе мы загрузим для имеющихся типов объектов и их полей, функции доступа и будем использовать их, и тем самым мы сможем минимизировать обращение к хеш-таблице, только одним доступом для каждого поля.

Итак наш синтаксис with-slots

;;Использование vfield приводит к многословности описания методов, можно ли это упростить? сократив код
;;возможный выход в макросе with-slots, определим его как:
(with-slots ((slot11 slot12 ..) obj1
             (slot21 slot22 ..) obj2
             ...)
            body)
;;slot это имя_слота или список (имя_для_обращения_к_слоту имя_слота) 
;;имя_для_обращения_к_слоту (превращается в)-> (vfield obj :имя_слота)
;;запись в слот описывается списком: (set! имя_для_обращения_к_слоту значение) -> (vfield! obj имя_слота значение)

(define-macro (with-slots slot-objs . body)
  (let* ((slots (parse-slot-objs slot-objs))
         (lets-and-body  (build-body-methods-with-slots slots body)))
    `(let (,@(car lets-and-body))
       ,@(cdr lets-and-body))))

Вот этот макрос и создаёт синтаксис упрощающий доступ к полям. шучу, тут есть ещё небольшая функциональная обвязка.

структуры и функции помогающие работать with-slots
;;структура описывающая определение алиаса, здесь он указан как name
(struct slot-def
  (name slot obj key name-get name-set))

;;разбор объявления полей или алиасов объектов
(define-m (parse-slot-objs slot-objs)
  (let ((rez '()))
    (do ((cur slot-objs (cddr cur)))
	((or (null? cur)
	     (null? (cdr cur))))
      (let ((obj (cadr cur)))
	    (for-list (el (car cur))
		  (let ((name (if (pair? el)
				  (car el)
				  el))
			(slot (if (pair? el)
				  (cadr el)
				  el)))
		    (let* ((key (make-symbol ":" slot)))
		      (push rez (slot-def! name slot obj key (gensym) (gensym))))))))
		  rez))

;;вспомогательные функции для работы с лямбда
;; move to util.scm
(define (tagged-list? exp tag)
  (and (pair? exp) (eq? (car exp) tag)))   ;;(list? exp)
(define (lambda? exp) (tagged-list? exp 'lambda))
(define (lambda-parameters lambda-exp) (cadr lambda-exp))
(define (lambda-body lambda-exp) (cddr lambda-exp))
(define (make-lambda params body)
  (let ((tmp-rez1 (cons 'lambda (cons params body))))
    tmp-rez1))


;;проодит по дереву выражения и выполняет замену имён переменных на геттеры и сеттеры полей слотов объектов.
(define-m (tree-expr-replace-get-set expr var build-get build-set)
  (let ((use-get #f)
	    (use-set #f)
	    (tree-expr-replace-get-set-rec #f))
    (set! tree-expr-replace-get-set-rec
	  (lambda (expr) 
	    (cond ((null? expr)
		   (begin
		     '()))
		  ((lambda? expr) ;;возможно в лямбде есть что заменить, например в замыкании, свободную переменную
		   (let ((params  (lambda-parameters expr)))
		     (if (cdr (find (lambda (elem) (eq? var elem)) params))
			  expr ;;одна из переменных лямбды затеняет нашу переменную var, дальше замену не делаем
			  (make-lambda params ;;коллизий нет, захвата переменной не будет. выполняем
				       (tree-expr-replace-get-set-rec (lambda-body expr))))
		     ))
		  ((eq? expr var)
		   (unless use-get (set! use-get #t))
		   (build-get))
		  ((and (pair? expr)   ;;(list? expr)
			(= (length expr) 3)
			(eq? 'set! (car expr))
			(eq? (cadr expr) var))
              (unless use-set (set! use-set #t))
		      (build-set (tree-expr-replace-get-set-rec (caddr expr))))
		   ((pair? expr)      ;;(list? expr)
		    (cons (tree-expr-replace-get-set-rec (car expr))
			      (tree-expr-replace-get-set-rec (cdr expr))))
		   (#t
		    expr))))
    (let ((rez (tree-expr-replace-get-set-rec expr)))
      (cons (list use-get use-set) rez))
  ))


;;преобразует тело, выполняя подстановку вместо имён ставит вызовы геттеров и сеттеров,
;;возвращает набор для лет оператора, используемых геттеров и сеттеров и преобразованное тело.
(define-m (build-body-methods-with-slots slots body)
  (let ((let-list '())
        (rez-body body)
        (tmp-rez  '()))
    (for-list (el slots)
        (set! tmp-rez (tree-expr-replace-get-set
                       rez-body
                       (slot-def-name el)
                       (lambda ()  `(,(slot-def-name-get el) ,(slot-def-obj el)))
                       (lambda (v) `(,(slot-def-name-set el) ,(slot-def-obj el) ,v))))
        (set! rez-body (cdr tmp-rez))
        (when (caar tmp-rez)
          (push let-list `(,(slot-def-name-get el)
                           (get-vfield-getter ,(slot-def-obj el) ,(slot-def-key el)))))
        (when (cadar tmp-rez)
          (push let-list `(,(slot-def-name-set el)
                           (get-vfield-setter ,(slot-def-obj el) ,(slot-def-key el)))))
        )
    (cons let-list rez-body)))

А теперь пример получившегося синтаксиса.

;;класс правил
(defclass FARule (Object)
  (state character next-state))
    
(defmethods FARule
  (applies-to? (st chr)
    (with-slots ((state character) self)
      (and (equal? state st) (char=? character chr))))
  (follow ()
   (with-slots ((next-state) self)
       next-state))
  (inspect (cycle)
    (with-slots ((state  character next-state)  self)
      (join-to-str "# #{" next-state "}&gt;")))
  )
;или вот с примером установки слота
(defclass DFA (Object)
  (start current-state accept-states rulebook))
  
(defmethods DFA
  (inspect (cycle)
    (with-slots ((current-state accept-states rulebook) self)
      (join-to-str "DFA: " current-state "/"
                   (avl-tree-get-ordered-values (set*-base accept-states)) "/"
                   (inspect rulebook cycle))))
  (accepting? ()
    (with-slots ((current-state accept-states) self)
       (set-has? accept-states current-state)))
  (reset ()
    (with-slots ((start current-state) self)
      (set! current-state start)))      ;;это установка слота объекта self
  )

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

Что можно ещё добавить? Разве что совет: Выбирайте имена алиасов полей как имя переменной тире имя поля. Либо если объект у вас один, просто оставляйте имена полей. Иначе алиасы вместо помощи будут вас путать.

Dot синтаксис.

Синтаксис обращения к полям через точку я реализовал для структур, в виде макроса with-stru, когда писал алгоритм работы с AVL-деревьями. Функции работы с бинарными деревьями очень много обращаются к полям узлов, которые в свою очередь являются аналогичными полями узлов и функциональный тип записи имеет большую вложенность вызовов аццессоров. Простой пример подсчёт высоты узла:

(define (avl-node-calculate-height node)
   (begin
      (avl-node-height! node (+ 1 (max (if (null? (avl-node-left node))
                                           0 (avl-node-height (avl-node-left node)))
                                       (if (null? (avl-node-right node))
                                           0 (avl-node-height (avl-node-right node))))))))

Вызов в одном месте нескольких аццессоров немного затрудняет чтение, хотелось бы получить такой синтаксис который удобно предоставлял доступ к полям структуры, как это делается в других языках программирования.

;;например
node.right

;; (with-stru
;;    (stru-var1 type-stru-var1 ;;описание переменных и типов структур
;;     stru-var2 type-stru-var2
;;     stru-var3 type-stru-var3
;;     ........)
;;    (set! stru-var1.fields11   value11)   ;;пример кода установки поля структуры
;;    (prn  stru-var1.fields12)
;;    ....
;;    body)

Для реализации возможности подобного синтаксиса создан макрос.

(define-macro (with-stru var-stru . body)
  (let* ((vars   (parse-var-stru var-stru body))  
         (new-body  (tree-expr-replace-vars-dot-fields body vars))) ;;
    `(begin ,@new-body)))

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

функции поддерживающие работу макроса.
;;находит в строке символ, начиная с конца строки
(define-m (string-at-end str ch)
   (let ((len (- (string-length str) 1)))
      (do ((i len (- i 1)))
            ((or (< i 0)
                 (char=? (string-ref str i) ch))
             (if (< i 0) #f i)))))

;;разбивает символ на два символа если в его имени есть точка, разбиение происходит с точек находящихся ближе к концу.
(define (split-dot-symbol symb)
   (let* ((str (symbol->string symb))
          (len (string-length str)))
      (if (> len 2)
          (let ((pos (string-at-end str #\.)))
             (if (and pos (> pos 0) (< pos (- len 1)))
                 (cons (string->symbol (substring str 0 pos))
                       (string->symbol (substring str (+ pos 1))))
                 #f))
          #f)))


(struct var-stru-def
  (var type))


;;добавили возможность определять для несколькоих переменных одну структуру, задвая список а не конкретныую
;;переменную
(define-m (parse-var-stru var-stru)
  (let ((rez '()))
    (do ((cur var-stru (cddr cur)))
          ((or (null? cur)
               (null? (cdr cur))))
       (let ((var         (car cur))
             (type-stru   (cadr cur)))
          (if (pair? var)
              (for-list (el (reverse var))
                    (push rez (var-stru-def! el type-stru)))
              (push rez (var-stru-def! var type-stru)))))
    rez))

(struct field-def
  (name field var name-get name-set))


(define-m (tree-expr-replace-vars-dot-fields expr var-list)
   (let ((aliases     (make-hash 16))
         (tree-expr-replace-get-set-rec #f)
         (is-alias-fields #f)
         (fd              #f) ;;переменная определения поля
         (in-var-list (lambda (var var-list) (find (lambda (x) (eq? ( var-stru-def-var x ) var)) var-list)))
         (exclude-params (lambda (var-list params)
                            (remove-if (lambda (var)
                                          (cdr (find (lambda (elem) (eq? (var-stru-def-var var) elem)) params))) var-list))))
      (set! is-alias-fields
         (lambda (expr var-list)
            (if (symbol? expr)
                (let ((rez (hash-ref aliases expr))) ;;проверяем хеш.
                   (if (car rez)
                       (cdr rez) ;;вернём результат, он пригодиться функциям построения get и set
                       (let ((var-field (split-dot-symbol expr))) ;;попробуем "разбить" символ.
                          (if var-field
                              (let ((in-var (in-var-list (car var-field)  var-list)))
                                 (if (cdr in-var)
                                     (let* ((getter-name  (make-symbol
                                                           (var-stru-def-type (car in-var))
                                                           "-" (cdr var-field)))
                                            (setter-name  (make-symbol
                                                           (var-stru-def-type (car in-var))
                                                           "-" (cdr var-field) "!"))
                                            (fd (field-def! expr (cdr var-field) (car var-field)
                                                            getter-name
                                                            setter-name)))
                                        (hash-set! aliases expr fd)
                                        fd);;вернём полученное описание поля
                                     #f)) ;;это не наша пара!!! мы такой переменной не задавали!
                              #f))));;не смогли разбить на пару переменная-поле
                #f))) ;;это даже не символ!
      (set! tree-expr-replace-get-set-rec
         (lambda (expr var-list) 
            (cond ((null? expr) '())
                  ((lambda? expr) ;;возможно в лямбде есть что заменить, например в замыкании, свободную переменную
                   (let* ((params  (lambda-parameters expr))
                          (local-var-list (exclude-params var-list params)))
                      (if (null? local-var-list)
                          expr ;;нет свободных переменных из var-list, дальше замену не делаем
                          (make-lambda params ;;возможно какие то переменные будут свободными, попробуем их заменить
                                       (tree-expr-replace-get-set-rec (lambda-body expr) local-var-list)))
                      ))
                  ((begin
                      (set! fd (is-alias-fields expr var-list))
                      fd)
                   (list (field-def-name-get fd) (tree-expr-replace-get-set-rec (field-def-var fd) var-list)))
                  ((and (pair? expr)   ;;(list? expr)
                        (= (length expr) 3)
                        (eq? 'set! (car expr))
                        (begin
                           (set! fd (is-alias-fields (cadr expr) var-list))
                           fd))
                   (list (field-def-name-set fd)
                         ;;(field-def-var fd)
                         (tree-expr-replace-get-set-rec (field-def-var fd) var-list)
                         (tree-expr-replace-get-set-rec (caddr expr) var-list)))
                  ((pair? expr) ;;(list? expr)
                   (cons (tree-expr-replace-get-set-rec (car expr) var-list)
                         (tree-expr-replace-get-set-rec (cdr expr) var-list)))
                  (#t
                   expr))))
      (let ((rez (tree-expr-replace-get-set-rec expr var-list)))
         rez)
      ))

таким образом функция подсчёта высоты узла бинарного дерева, с учётом нового синтаксиса, будет выглядеть так:

(define-m (avl-node-calculate-height node)
  (with-stru (node       avl-node
              node.left  avl-node
              node.right avl-node)
       (set! node.height (+ 1 (max (if (null? node.left)  0 node.left.height)
                                   (if (null? node.right) 0 node.right.height))))))

Нужно БОЛЬШЕ сахара!

Этот пример я делал в лиспе, когда нужно было "украсить" код одного алгоритма, решета эратосфена. В нём я ввёл синтаксис сокращённого доступа к элементам массива.

;;обычный лисп:
(aref a 2)

;;украшеный синтаксис:
a[2]

Как достичь такого синтаксиса? Также как и раньше, с помощью макросов!

(defmacro with-arr (&body body)
  (let ((new-body (tree-expr-replace-aref body)))
    `(progn ,@new-body)
    ))
вспомогательные функции
(defun tree-expr-replace-aref (expr)
  (let ((fd '()))
    (cond ((null expr) '())
          ((and
            (symbolp expr)
            (progn
              (setf fd (split-arrayindex-symbol expr))
              ;;(prn "fd: " fd "\n")
              fd))
           ;;(list (field-def-name-get fd) (field-def-var fd)))
           (list 'aref (tree-expr-replace-aref (car fd)) (cdr fd)))
          ((listp expr)
           ;;(prn "TR-REPL: find set!: " fd "\n")
           (cons  (tree-expr-replace-aref (car expr))
                  (tree-expr-replace-aref (cdr expr))))
          (t
           expr))))

(defun split-arrayindex-symbol  (symb)
   (let* ((str (symbol-name symb))
          (len (length str))
          (last (1- len)))
     (if (and (> len 3)
              (char= (char str last) #\]))
         (let ((pos (string-at-end str #\[)))
           (format nil "pos: ~A~%" pos)
           (if (and pos (> pos 0) (< pos last))
               (cons (read-from-string str nil nil :start 0 :end pos) 
                     (read-from-string str nil nil :start (+ pos 1) :end last))
               nil))
         nil)))

(defun string-at-end (str ch)
   (let ((last-ind (- (length str) 1)))
      (do ((i last-ind (- i 1)))
            ((or (< i 0)
                 (char= (char str i) ch))
             (if (< i 0) nil i)))))
;улучшить код алгоритма решета, если изменить макрос for, так чтобы он принимал шаг цикла, например так:
(defmacro for ((var start stop . step) &body body)
  (let ((gstop (gensym))
        (step  (if (null step) 1 (car step))))
    `(do ((,var ,start (+ ,var ,step))
          (,gstop ,stop))
         ((> ,var ,gstop))
       ,@body)))

И тогда код решета эратосфена на Лиспе будет выглядеть так:

(defun eratosfen2 (n)
  (let ((a (make-array (1+ n))))
    (with-arr
      (setf a[0] nil)
      (setf a[1] nil)
      (for (i 2 n)
        (setf a[i] i))
      (for (i 2 (floor (sqrt n)))
        (when a[i] 
          (for (j (* i i) n i)
            (setf a[j] nil))))
      (remove nil a))))

Выводы

На самом деле все авторы языков программирования изобретают новые синтаксисы улучшая выразительность языков. В компилируемых языках это происходит почти бесплатно, для конечного результата, а в интерпретируемых, бывает код раскрытия нового синтаксиса нагружает интерпретатор, поэтому в таких языках синтаксические излишества замедляют выполнение. Разработчики Scheme(а вместе с ней и Script-fu) предпочли ограничиться минимальным синтаксическим набором для интерпретатора, но вместе с тем дали мощный инструмент развития синтаксиса в руки программиста, который может по своей прихоти и конечно же по необходимости вводить в язык новые языковые конструкции. При этом если раскрытие макроса происходит заблаговременно(как это делает макрос define-m, а на самом деле можно формировать уже "раскрытые" файлы программ), то такие языковые расширения проходят бесплатно и для процесса интерпретации программы.

Tags:
Hubs:
0
Comments4

Articles