Skip to content

Script-fu/Tinyscheme Macros

The Tinyscheme interpreter upon which GIMP Script-fu relies is to a large degree compliant with the R5RS specification, surprisingly so, given its small memory footprint (<100Kb). One of the few areas where it deviates from the standard is with regard to macros -- Tinyscheme does not implement the "syntax-rules" pattern-matching functionality of R5RS, nor does it directly implement "hygienic" macros. I will postpone any discussion of these two concepts until after we've addressed the macro facilities that Tinyscheme(/Script-fu) does provide.

What is a "macro"?

In Scheme, it is a general rule that all arguments get evaluated before they are passed to a procedure. For example, in the following statement, the mathematical expressions "(- 14 4)" and "(* 3 5)" each get evaluated (to "10" and "15", respectively) before being passed to the addition function, which in turn gets evaluated (to "25") and passed to the display procedure.

(display (+ (- 14 4) (* 3 5)))

This general behavior is quite adequate for most Scheme programming, however, it is often convenient to NOT evaluate the arguments being passed, but instead leave the decision whether or not to evaluate the arguments to the processing within the procedure. Consider the statement

(set! x 10)

In this situation, we do not want the first argument to be evaluated (i.e., we don't want to access the original value assigned to 'x'), but instead want to treat it as a symbol.

(if (< x 0) (display "X is a negative number"))

Here we don't want the message to be displayed before the 'if' routine is invoked -- we only want it to be displayed after the 'if' code has evaluated the expression "(< x 0)" and found it to be true. In other words, we don't want to evaluate the third argument until after the second argument has been evaluated and ensured to be true.

Both of these operations, "set!" and "if", are core to the Scheme language but they demonstrate how evaluation of arguments can be postponed, or even skipped altogether. They are not properly called "procedures" or "functions" but instead are called "special forms" because they follow special evaluation rules (i.e., their arguments are not always evaluated).

Scheme macros permit you the programmer to define your own "special forms" and thereby increase the expressiveness and flexibility of your programs. As is the case with variable arguments discussed in my previous article, you will rarely find it necessary to use macros when writing GIMP scripts, however, it is a powerful technique that can enhance your scripting skills.

The 'macro' statement

The basic functionality of macros in Tinyscheme are implemented with a 'macro' operator. I have not found very much documentation on this, but through examination of the source code and some experimentation have figured out its behavior (at least I think so).

A macro definition has the basic following form:

(macro (NAME FORM)
  *BODY* 
  )

When a macro call is encountered (while reading your script), the BODY of the macro is executed (evaluated) and the output (a Scheme expression) substituted for the macro invocation. The entire macro invocation expression is available with the body as the variable "FORM".

(macro (foo form)
  (list 'quote form)
  )
(foo (* (+ 5 2) 3)) ==> (foo (* (+ 5 2) 3))

Note that if we'd defined 'foo' as a procedure then the formula would have been evaluated (to "21") before the procedure code was called. With a macro, the arguments are passed directly without any evaluation taking place.

Also, notice that the entire expression, including the macro name, is passed to the body. This threw me at first because I would've expected just the unevaluated arguments to be passed. It ends up that there is a good reason for doing this (which comes into play when creating nested or recursive macros), but by and large most macros will just skip over the macro name and only use the arguments.

In addition, all of the arguments are included in the 'form' variable. There is no need to specify separate names for each expected argument when defining your macro. In fact, if you were to define a macro with "(macro (foo x y z)" it would generate an error everytime it was invoked. (You can apparently leave out the FORM variable when defining a macro, but that would be useless.)

The beauty of Scheme macros arises from the fact that all code is in the form of symbolic expressions. In other words, any block of code is just a list, and you can access any part of that code using just the 'car' and 'cdr' functions (no need for complicated parsing, token evaluation, lookaheads, or checking of indentation levels). The job of a body of a macro is to examine the code and produce new code by re-arranging and/or adding to those elements. This new code is, of course, a list; and so the output of the body of a macro is a list.

An Example

Most new-comers to Scheme struggle a little bit with the syntax of its IF statement.

(if predicate
    consequent
    alternative )

It is not uncommon to forget that the 'consequent' (what to do if the 'predicate' test is true) is a single expression. If you wish to do more than one thing then you generally need to combine all of your actions into a single "block", otherwise only the first action will be performed and the second one treated as the 'alternative' (what to do if the 'predicate' is false).

Using Tinyscheme's macro facility, we can easily introduce a new conditional which allows multiple expressions to be evaluated (i.e., actions to be taken) when the 'predicate' is true.

(macro (when form)
  (list 'if (cadr form) 
            (cons 'begin (cddr form)) ))

We now have a new command (actually, Tinyscheme already offers this macro, but it serves as a fine example of a simple macro).

(when (< x 0)
  (display "Negative numbers are not allowed")
  (newline)
  (display "Please enter a number greater than or equal to zero")
  (newline) )

Quasiquoting

Scheme offers a very powerful facility called "quasiquoting" for creating lists that is especially useful for creating macros -- because the output of a macro body is expected to be a list. Quasiquoting is quite well-documented and so I won't go into too much detail about it, but the general premise is that code that has been quasiquoted will by default not be evaluated (just as with quoting), however, you can optionally 'unquote' parts of the expression.

This form of list generation generally results in macros that are much more recognizable with regard to the code being generated. For example, the body of the preceding 'when' macro would appear as follows in quasiquote notation:

  `(if ,(cadr form)
     (begin
       ,@(cddr form) ))

NOTE: this is precisely how the 'when' macro is defined in GIMP's "script-fu.init" file.

What about GIMP?

Just as in the case of variable number of arguments, it will rarely be worthwhile to create macros for typical Script-fus. The problem is that while macros can make your code more readable, the reader needs to learn what the macro does. Unless your macro is going to be made available globally to all Script-fus (and this introduces the potential of namespace collisions), it is usually just better to "brute force" your code.

Nonetheless, I will present a few examples of macros that might assist in making Script-fu code more readable, or at least a bit more "Scheme-y". (Maybe in the future, some useful macros can be introduced into the default GIMP installation so that script writing will become easier.)

;; (with-context
;;   *BODY* ) 
;
(macro (with-context form)
  `(begin
     (gimp-context-push)
     ,@(cdr form)
     (gimp-context-pop) ))

;; (with-undo image
;;   *BODY* )
;
(macro (with-undo form)
  `(begin
      (gimp-image-undo-group-start ,(cadr form))
      ,@(cddr form)
      (gimp-image-undo-group-end ,(cadr form)) ))

Wrap-up

This article is already getting overly long so I will leave to future installments any further coverage of Tinyscheme macros and the potential they present to improving GIMP scripting.

Regards.

 

Add A Comment

Name:
Email:
Website:
Your Comment

Your submission will be ignored if the name, email, or comment field is left blank.

Your email address will never be displayed, but your homepage will be.