Skip to content

Entries from November 2011.

Variable Arguments in Script-fu (Scheme)

I thought I'd briefly describe how one can define Script-fu procedures which accept a variable number of arguments and how those arguments are handled.

There are two basic ways of defining a new procedure in Scheme:

(define (somefunction x y z)
  ...
  )

or

(define somefunc
  (lambda (x y z)
    ...
    )
  )

The two forms are effectively equivalent; in both cases a lambda closure is created and then bound to a symbol. In fact the former is generally considered merely syntactic sugar for the latter. Nonetheless, there are potential advantages to the second ("lambda") form in certain situations.

One advantage obtains from the fact that operations can be inserted before the lambda is created. For example, one might check to see if a desired dependency is available.

(define somefunc
  (let ((align-down-missing? 
          (zero? (car (gimp-procedural-db-proc-exists 
                        "script-fu-sg-align-down" )))))
    (lambda (x y z)
      ...
      )
    )
  )

The test for the existence of the Align Down script only has to be performed once when the function is first defined. Only the lambda part will be executed when 'somefunc' is invoked, yet the variable representing the missing status will be retained forever. In Scheme terminology, the 'align-down-missing?' binding exists in the environment captured by the lambda (the environment which existed at the time the lambda was created), but not in the global environment. Of course, one could assign 'align-down-missing?' as a variable in the global environment (by definining it outside the 'somefunc' definition), but global variables tend to clutter things up and can introduce potential problems with namespace conflicts. There are other interesting aspects of this "capturing" of environments by a lambda binding, but I will leave those for a later discussion.

The main point I wish to cover is passing a variable number of arguments to procedures. This is implemented in Scheme by using a notation rather similar to that of dotted pairs. For example, if we have the definition

(define (somefunc . z)
  ...
  )

then within the definition, the variable 'z' will contain a list of all the arguments that were passed to the procedure. For example, if you invoked the above using "(somefunc 1 2 3 4)" then the variable 'z' would be assigned the list '(1 2 3 4). You can access individual elements of that list using the traditional 'car' and 'cdr' functions.

If you included parameters before the dot, for example,

(define (somefunc x y . z)
  ...
  )

These "explicit" parameters would be assigned their passed values and the rest of the arguments would be assigned (as a list) to 'z'. If you only provide the explicit arguments 'x' and 'y' then the list 'z' will be empty. Note that when calling your procedure, you must provide all explicit arguments.

It should be noted that while the first example above works fine for defining a procedure with no explicit arguments, this notation does not work in Scheme when the lambda form is employed. In other words, the following code will NOT work:

(define somefunc
  (lambda ( . z)
    ...
    )
  )

Instead, to employ the lambda form with no explicit arguments, omit the parentheses and period altogether, leaving only the identifier to which the list of arguments is assigned.

(define somefunc
  (lambda z
    ...
    )
  )

Variable arguments can provide a nice way of expanding the functionality of a procedure without increasing the complexity of the default behavior. Consider the following procedure whose main purpose is to duplicate an object -- whether that object be a number, a string, a character, or a list. If a second argument is supplied, that argument should be the number of copies that result (if omitted, this is assumed to be "2"). If a third argument is supplied (and has a non-false value), it indicates that cloning of characters or strings should result in a single, concatenated string (not a list).

; Make list comprising N copies of the item (default N = 2)
; ex:  (clone 9)   ==> (9 9)
;      (clone 9 4) ==> (9 9 9 9)
;      (clone "abc" 3) ==> ("abc" "abc" "abc")
; chars and strings accept a third arg which if not false,
; will result in a string instead of a list.      
; ex:  (clone #\a 4)     ==> (#\a #\a #\a #\a)
;      (clone #\a 4 #t)  ==> "aaaa"
;      (clone "ab" 3 #t) ==> "ababab"

(define (clone x . args)
  (let ((count (if (null? args)
                 2
                 (car args) ) ))
    (if (and (pair? args) 
             (pair? (cdr args))
             (cadr args) )
      (let loop ((result "")
                 (count count))
        (if (zero? count)
          result
          (if (char? x)
            (loop (string-append result (make-string 1 x)) (- count 1))
            (loop (string-append result x) (- count 1)) )))
      (let loop ((result '())
                 (count count) )
        (if (zero? count)
          (if (null? result)
            '()
            (reverse result) )
          (loop (cons x result) (- count 1)) )))))

Combining the functionality of variable arguments with Script-fu's dynamic typing of variables, it is possible to write some rather elegant procedures. For example,

(define (paint-xy drawable x y . args)
  (let ((color (cond
                 ((null? args) ;; color not specified, use FG
                   (list->vector (car (gimp-context-get-foreground))) )
                 ((vector? (car args)) ;; color is specified as a vector
                   (car args) )
                 ((pair? (car args)) ;; color is specified as a list
                   (list->vector (car args)) )
                 ((string? (car args)) ;; color specified using its CSS name
                   (let ((color '(0 0 0)))
                     (gimp-context-push)
                     (gimp-context-set-foreground (car args))
                     (set! color (list->vector (car (gimp-context-get-foreground))))
                     (gimp-context-pop)
                     color ))
                 (else ;; red, green, and blue values provided inline
                   (list->vector args) ))))
     (gimp-drawable-set-pixel drawable x y (vector-length color) color) ))

This single function could be invoked in any of the following ways:

(paint-xy layer x y)                ;; using current foreground color setting
(paint-xy layer x y #(255 255 255)) ;; white
(paint-xy layer x y '(0 255 0))      ;; green
(paint-xy layer x y "aliceblue"))   ;; CSS named color
(paint-xy layer x y 255 165 0)      ;; orange

For those interested, the color names recognized by GIMP are listed in the gimprgb-parse.c file in the source tree.

While it is somewhat rare for GIMP scripts to attain the level of complexity where the full power of the Scheme programming language can be exploited, it is nonetheless useful to learn of its capabilities.

Regards.

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.