GIMP Script-Fu First Dan. Macros. First acquaintance

As we initially found out, the script-fu ecosystem consists of built-in functions (about one and a half hundred) and special forms. Downloadable initialization script: /usr/share/gimp/2.0/scripts/script-fu.init, which also contains about one and a half hundred definitions.

No need to count by hand.

grep -e "^(define" script-fu.init | wc -l ;;132

But this is insanely not enough for a full-fledged programming language. How can this be? Lisp allows two approaches to language development: functional abstraction, when we create functions based on existing functions, and syntactic abstraction, when we create syntactic forms by writing macros.

Programmers: Developer, your language is pathetic, the syntax is poor and wretched, how should we write code?!?
Developer (lisp language): Here you go MACRO!!!

Programmer and Language Developer.

Programmer and Language Developer.

The developers of the Lisp language (and Scheme too) were very cunning guys, shamelessly taking advantage of homoiconicityyu of the language (that is, the property of code similarity to data – the famous phrase in Lisp, the whole list refers precisely to this property), they introduced macros into the language, means of syntactic abstraction.

Thus, unlike most of the currently widespread programming languages ​​(whose developers do not sleep at night, writing parsers for more and more new syntactic forms of their languages), Lisp developers make do with a minimal set of syntactic forms, which simplifies and speeds up the process of interpretation (and compilation) too), and the rest of the richness of the syntax is left to macros. It is through macros, which are an orthogonal means of abstraction, in relation to the functional abstraction available in all programming languages, that the improvement and development of Lisp languages ​​is realized. This can be described by a mathematical analogy, when in mathematics there was a transition from rational numbers (only functional abstraction is available in the language) to complex numbers (a system of syntactic abstraction – a macro – is introduced into the language).

There is a funny story connected with the emergence and consolidation of this homoiconicity in Lisp.

Initially, the inventor of the Lisp language, McCarthy, planned to build Lisp from Sexpr and Mexpr expressions, so to speak, primitive expressions and META expressions. Let me show you:

;;Sexp выражения
(apply f1 12 "stre" 'sym)

;;Mexpr - аналог Sexp
apply[f1;12;"stre";(quote sym)]

;;Mexpr манипулирует sexp как данными:
eval[(EQ (QUOTE A) (CAR (CONS (QUOTE A) (QUOTE (B C D)))));NIL]

It was planned that Sexpr would be primitive Lisp expressions, this is something like data that would be manipulated by meta expressions – Mexpr. But with the implementation of sexpr and the gradually accumulated practice of programming on them, it turned out that no Mexpr was required, any necessary functionality could be expressed through sexp, so Mexpr never appeared in Lisp. So to speak, a simpler syntax (but no less expressive) won.

The difference between Lisp syntax and C-like languages

Indeed, this is what Lisp is usually blamed for: you have too many parentheses. Why so many? Let's compare the Lisp and C function calls:

;;Лисп
(f1 1 2 3)

;;Си
f1(1 2 3)

there is no difference, there are the same number of brackets, only the function name in C is placed outside the brackets, and in Lisp inside the brackets. Of course, in C there are operators that have infix notation and predefined priorities, which make it possible to get rid of a large number of parentheses in mathematical expressions, but in some cases, here too, Lisp sometimes outperforms the C syntax in terms of expressiveness.

;;Лисп
(set! rez (+ 1 2 3 4))

;;Си
rez = 1 + 2 + 3 + 4;

;;Лисп
(set! ret (if (= 0 value)
              (f1 1 2)
              (f2 1 2 3))) 

;;Си
if (0 == value) {
    ret = f1(1 2);
} else  {
    ret = f2(1 2 3);
}

but due to the operator syntax, parsing C expressions involves the need to have all sorts of parsers and lexers. Comparing expressions if we will find exactly 10 brackets in each, although in Lisp there is a longer “tail of brackets” sticking out, but in fact their number is the same.

And the presence of this operator syntax seriously complicates the implementation of a fully functional macro system in such languages. Although attempts to implement such an implementation are certainly being made. As an example of this approach, I can cite the Julia language. He also acts cunningly: he parses the input expression with a parser, turns it into Lisp-like code, i.e. homoiconic code, code in the form of data, then the programmer is asked to convert it, and then transfer the created code, either for insertion into the code as an expression, or for evaluation or compilation, if this happens at the top level of the REPL, but in C the code this converted expression is no longer translated, which is not very convenient for programmers; they have to operate with both C syntax and Lisp syntax. In detail

In general, my acquaintance with Julia gave me the impression that it was Lisp with Mexpr implemented in it. Although, in my opinion, the language turned out to be overcomplicated and all because of an attempt to introduce macros. But I liked the idea of ​​marking macros with the @ sign.

The main difference between macros and functions.

Unlike functions, macro arguments are not evaluated before being passed to the macro expansion function, but are passed as is. And the second important difference is that the result of executing a macro is not assigned to any variable, but is inserted into the code of the function body (or passed for evaluation if it is expanded in the REPL) at the place where the macro is called.

An extravagant example of using macros.

There are cases in Lisp when you need to work with nested data structures, one might even say recursive, tree-like, and there the number of getters and setters turns out to be very significant and such functional Lisp syntax turns out to be difficult to read, but it happens. But Lisp has its own answer to this – MACROES!

I’ll give an example without revealing the essence of macros: Recently I was working with binary trees, and more specifically, writing functions for working with AVL trees, where I had to work with several nodes of a binary tree at once, and getters and setters began to ripple in my eyes, then I went and “borrowed” ” dot syntax from C-like languages:

;;без dot-синтаксиса
(define-m (avl-node-calculate-height node)
    (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)))))))

;;с dot-синтаксисом
(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))))))

It seems that there is no point in the dot syntax, the code is approximately the same in size, but it only seems, the working logic takes up much less space and is more compact, the meaning of accessing the fields is clearer. The only vulnerable (to criticism) place is the “header” in which the types of variables are defined, what could you come up with in this case? For such monotype and many variable records, the following syntax would be perfect:

(define-m (avl-node-calculate-height node)
  (with-vars-stru (node node.left 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))))))

Implementing the with-var-stru syntax based on the existing with-stru is not difficult at all.

(define-macro (with-vars-stru vars-stru type . body)
   `(with-stru ,(fold (lambda (lst el) (cons el (cons type lst))) '() (reverse vars-stru)) ,@body))

Another question is, is it worth it? Is it worth creating syntaxes just because we can do it? It seems to me that in this case it would be worth improving the syntax of the original macro with-stru. Initially macro with-stru I analyzed definitions that have the structure: (structure-type variable, structure-type variable…).

adjust with-stru
;;сам код макроса - как видите он очень простой.
(define-macro (with-stru var-stru . body)
  (let* ((vars   (parse-var-stru var-stru body))   ;;(parse-slot-objs fields-stru)
         (new-body  (tree-expr-replace-vars-dot-fields body vars))) ;;
    `(begin ,@new-body)))

;;функция разбирающая определения связки переменная - структура
(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)))     ;;а здесь имя типа структуры.
          (push rez (var-stru-def! var type-stru))))
   rez))

and now we needed to parse definitions, where it is possible to associate not only a variable with the name of a structure, but also a list of variables. Let's change the parsing function:

(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))

and in accordance with the new behavior of the macro we can write:

(define-m (avl-node-calculate-height node)
  (with-stru ((node node.left 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))))))

What was this example for? To show that homoiconicity of code is an important aid in writing macros and thereby developing the syntactic expressiveness of the language.

How to write macros.

In essence, macros are ordinary Scheme procedures; the only requirement for them is that they must return some syntactically meaningful code.

Before writing code, you need to initialize the environment to make it more convenient to work:

;;(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"))

Our first macro contains code that is executed when the macro is expanded (for now this is a simple print) and the resulting code that will be returned as the result of the macro:

(define-macro (test1 var)
   (prn "My first macro: run expand: " var "\n")
     `(begin
        (prn "test print: " ,var "\n"))
   )

(test1 "my string")
;;My first macro: run expand: my string
;;test print: my string
;;#t

At first glance, there is no visible difference between a function and a macro. Well, our macro doesn’t do anything special, but what exactly it does can be seen using the macro expansion command:

(macro-expand '(test1 "My string"))
;;My first macro: run expand: My string
;;(begin (prn "test print: " "My string" "\n"))

that is, it runs a macro, which, being a regular function, prints a string and returns the syntactic construction “(begin …”. And if we set a command in the interpreter console (test1 “My string”), then the returned code is immediately interpreted and another one occurs seal.

Typical macros

Let's look at creating a more complex macro. For example, if we need to process arrays or ranges of numbers, then the for statement that lists the indices in a given range is very suitable for processing them, and we would like to have something like a syntactic construct like this:

;;(for (i 1 10)
;;    (prin1 "i: ") (print i))

A fairly compact entry, without the need to write any conditional checks. To implement it, you can create a macro, for example like this:

(define-macro (for var . body)
   ;;(prn "Раскрытие макроса for\n")
   (let ((i     (car var))
         (start (cadr var))
         (end   (caddr var)))
      `(let loop ((,i ,start))
          (cond
           ((<= ,i ,end)
            ,@body
            (loop (succ ,i))
            )))))
          
(for (i 1 10)
   (prin1 "i: ") (print i))
          
(for (i 1 10)
   (prin1 "i: ") (print i)
   (for (j 2 (/ i 2))
      (prin1 "   j: ") (prin1 j))
   (newline))

The main work of creating the result occurs under the backquote sign[`]which is syntactic sugar for the macro quasiquote (quasi citation), which, unlike ordinary citation (quote – and this is no longer a macro but a special form), allows you to perform Lisp functions.

There is a double beauty in writing macros: firstly, you create a convenient syntactic structure, all the work of which is hidden in the definition of the macro, and secondly, you create a single point for working on syntax in your programs. If in the future it turns out that in some situations this construction makes an error, then you will only need to correct your macro, without changing the code of all expressions where this syntactic template was used. This is the same as with functions, but at the level of programming language syntax.

Or here’s another similar example, a loop going through the elements of a list:

;;(for-list (elm '(1 2 3 4 5))
;;    (prin1 "elm: ") (print elm))

(define-macro (for-list var . body)
   (let ((elm   (car var))
         (lst   (cadr var))
         (cur   (gensym)))
      `(let ((,elm nil))
          (let loop ((,cur ,lst))
             (cond
              ((not (null? ,cur))
               (set! ,elm (car ,cur))
               ,@body
               (loop (cdr ,cur))
               ))))))

I would like to note that the function is defined in the tinischeme for-eachbut this is precisely a function for which you need to create a lambda function that processes each element of the list.

for-each

(for-each (lambda (elm) (prin1 "elm: ") (print elm) (+ 1 elm)) '(1 2 3 4 5)) ;;"elm: "1 ;;"elm: "2 ;;"elm: "3 ;;"elm: "4 ;;"elm: "5 ;;(3 4 5 6)

Well, let's test the macro for processing all list elements:

(for-list (cur '(1 2 3 4 5)) (prin1 "cur: ") (print cur)) ;;"cur: "1 ;;"cur: "2 ;;"cur: "3 ;;"cur: "4 ;;"cur: "5 (for-list (cur '((1 2 3) (1 2) (a b c) (e f e))) (prin1 "cur: ") (for-list (cur2 cur) (prin1 " ") (prin1 cur2)) (newline)) ;;"cur: "" "1" "2" "3 ;;"cur: "" "1" "2 ;;"cur: "" "a" "b" "c ;;"cur: "" "e" "f" "e

Typical mistakes when writing macros.

Everything is fine, everything works, nested loops also work, except for one nuance, loop abstraction for-list – “leaks”. What does it mean? This means that the macro is designed in such a way that the user of this macro can consciously or unconsciously influence its operation by performing a completely legitimate action from their point of view. In this case, the label loop“sticks out,” that is, it is visible to the macro user’s code, and if he writes something like: (loop '(23 12))I don’t know why, the behavior of the cycle will become unpredictable.

In my case, the script-fu plugin simply froze.
(for-list (cur '(1 2 3 4 5))
    (prin1 "cur: ") (print cur)
	(loop '(23 12)))

But it’s easy to fix the macro and “fix the abstraction leak”; for this you need the macro to generate the label name itself using gensym.

(define-macro (for-list var . body)
   (let ((elm   (car var))
         (lst   (cadr var))
         (cur   (gensym))
         (lp    (gensym)))
      `(let ((,elm nil))
          (let ,lp ((,cur ,lst))
             (cond
              ((not (null? ,cur))
               (set! ,elm (car ,cur))
               ,@body
               (,lp (cdr ,cur))
               ))))))

(define-macro (for var . body)
   (let ((i     (car var))
         (start (cadr var))
         (end   (caddr var))
		 (lp    (gensym)))
      `(let ,lp ((,i ,start))
          (cond
           ((<= ,i ,end)
            ,@body
            (,lp (succ ,i))
            )))))
Hidden text
(define (loop a)
    (print a))
	
(for-list (cur '((1 2 3) (1 2) (a b c) (e f e)))
    (prin1 "cur: ")
    (for-list2 (cur2 cur)
              (prin1 "   ") (prin1 cur2))
    (newline)
	(loop '(5)))

;;"cur: ""   "1"   "2"   "3
;;(5)
;;"cur: ""   "1"   "2
;;(5)
;;"cur: ""   "a"   "b"   "c
;;(5)
;;"cur: ""   "e"   "f"   "e
;;(5)

Well, as you can see from the example, now the macro does not react in any way to the presence of a loop call in it. And to guess what value the gensym function will give to the label, you still have to try, and I think not every user will be so malicious as to ruin his life in this way.

So with these examples I wanted to show that despite all the limited functionality of the tinischeme, we have a powerful means of its development. While studying Gimp, I did not set out to write a library of functions or macros for the tinischeme; all of them were written in the process of working to build a functional geometry language.

For a more detailed introduction to the principles of writing macros, I recommend my humble translation of Paul Graham's book On Lisp

And here's another tip: NEVER use MACROS in Script-fu until you read the next article!!!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *