Next: , Previous: Differences Between Iterate and Loop, Up: Top

7 Rolling Your Own

7.1 Introduction

iterate is extensible—you can write new clauses that embody new iteration patterns. You might want to write a new driver clause for a data structure of your own, or you might want to write a clause that collects or manipulates elements in a way not provided by iterate.

This section describes how to write clauses for iterate. Writing a clause is like writing a macro. In fact, writing a clause is writing a macro: since iterate code-walks its body and macroexpands, you can add new abstractions to iterate with good old defmacro.

Actually, there are two extensions you can make to iterate that are even easier than writing a macro. They are adding a synonym for an existing clause and defining a driver clause for an indexable sequence. These can be done with defsynonym and defclause-sequence, respectively. See Extensibility Aids.

The rest of this section explains how to write macros that expand into iterate clauses. Here's how you could add a simplified version of iterate's multiply clause, if iterate didn't already have one:

  (defmacro multiply (expr)
    `(reducing ,expr by #'* initial-value 1))

If you found yourself summing the square of an expression often, you might want to write a macro for that. A first cut might be

  (defmacro sum-of-squares (expr)
    `(sum (* ,expr ,expr)))

but if you are an experienced macro writer, you will realize that this code will evaluate expr twice, which is probably a bad idea. A better version would use a temporary:

  (defmacro sum-of-squares (expr)
    (let ((temp (gensym)))
      `(let ((,temp ,expr))
         (sum (* ,temp ,temp)))))

Although this may seem complex, it is just the sort of thing you'd have to go through to write any macro, which illustrates the point of this section: if you can write macros, you can extend iterate.

Our macros don't use iterate's keyword-argument syntax. We could just use keywords with defmacro, but we would still not be using iterate's clause indexing mechanism. Unlike Lisp, which uses just the first symbol of a form to determine what function to call, iterate individuates clauses by the list of required keywords. For instance, for... in and for... in-vector are different clauses implemented by distinct Lisp functions.

To buy into this indexing scheme, as well as the keyword-argument syntax, use defmacro-clause:

— Macro: defmacro-clause arglist &body body

Defines a new iterate clause. arglist is a list of symbols which are alternating keywords and arguments. &optional may be used, and the list may be terminated by &sequence. body is an ordinary macro body, as with defmacro. If the first form of body is a string, it is considered a documentation string and will be shown by display-iterate-clauses. defmacro-clause will signal an error if defining the clause would result in an ambiguity. E.g. you cannot define the clause for... from because there would be no way to distinguish it from a use of the for clause with optional keyword from.

Here is multiply using defmacro-clause. The keywords are capitalized for readability.

  (defmacro-clause (MULTIPLY expr &optional INTO var)
    `(reducing ,expr by #'* into ,var initial-value 1))

You don't have to worry about the case when var is not supplied; for any clause with an into keyword, saying into nil is equivalent to omitting the into entirely.

As another, more extended example, consider the fairly common iteration pattern that involves finding the sequence element that maximizes (or minimizes) some function. iterate provides this as finding... maximizing, but it's instructive to see how to write it. Here, in pseudocode, is how you might write such a loop for maximizing a function F:

  set variable MAX-VAL to NIL;
  set variable WINNER to NIL;
  for each element EL in the sequence
      if MAX-VAL is NIL or F(EL) > MAX-VAL then
          set MAX-VAL to F(EL);
          set WINNER to EL;
      end if;
  end for;
  return WINNER.

Here is the macro:

  (defmacro-clause (FINDING expr MAXIMIZING func &optional INTO var)
    (let ((max-val (gensym))
          (temp1 (gensym))
          (temp2 (gensym))
          (winner (or var iterate::*result-var*)))
         (with ,max-val = nil)
         (with ,winner = nil)
          ((null ,max-val)
           (setq ,winner ,expr)
           (setq ,max-val (funcall ,func ,winner))
           (let* ((,temp1 ,expr)
                  (,temp2 (funcall ,func ,temp1)))
             (when (> ,temp2 ,max-val)
               (setq ,max-val ,temp2)
               (setq ,winner ,temp1))))))
         (finally (leave ,winner)))))

Note that if no into variable is supplied, we use iterate::*result-var*, which contains the internal variable into which all clauses place their results. If this variable is bound by some clause, then iterate will return its value automatically; otherwise, nil will be returned.