Previous Table of Contents Next


Here’s a simple example of threads at work:

   (define (counter n)
     (if (= (remainder n 20) 0) (exit-fork))
     (display n)
     (newline)
     (if (= (remainder n 5) 0) (wait))
     (counter (+ n 1)))
   
   > (together (lambda () (counter 6)) (lambda () (counter 201)))
   6
   7
   8
   9
   10
   201
   202
   203
   204
   205
   11
   12
   13
   14
   15
   206
   207
   208
   209
   210
   16
   17
   18
   19
   211
   212
   213
   214
   215
   216
   217
   218
   219
   :finished

It is tempting to think of call/cc as analogous to a label in other languages, and the continuation that it returns as a goto that jumps to the corresponding label. This idea is partly right and partly wrong. It’s right in that continuations can be used to create any desired flow of control, as goto can. But continuations are safer and cleaner than goto because they don’t just jump to another point in the program; they also remember and restore the correct environment—the scope of variables and the procedure call stack—to allow the continued program to run correctly.

3.4.4. Lexical Scoping Rules for Macros

It’s annoying that in the thread example, I had to say

   (together (lambda () (counter 6)) (lambda () (counter 201)))

using lambda to turn each of the expressions I wanted to compute into a procedure with no arguments. I really want to be able to say

   (together (counter 6) (counter 201))

but if I’d written together that way, Scheme would evaluate the two invocations of counter serially before running together, which sets up the threads.

Every dialect of Lisp provides some way to allow the programmer to extend the syntax of the language. Such a syntactic extension is called a macro. Each dialect of Lisp (and even, until recently, each implementation of Scheme) uses a different notation for macros. In the one that’s conceptually simplest, a macro is a procedure with two special characteristics. First, the macro is invoked with one argument, whose value is the entire unevaluated expression starting with the macro’s name. For example, if I write a together macro in the preceding example, it is invoked with a three- element list whose first element is the symbol together and whose other elements are themselves two-element lists, (counter 6) and (counter 201). Second, the macro must return a valid expression, and that expression is evaluated in place of the macro invocation. Here’s how you can write a together macro in one version of that notation:

   (define-macro (together exp)
     (append ‘(begin)
             (make-forks exp)
             ‘((waiter))))
   
   (define (make-forks tasks)
     (if (null? tasks)
         ‘()
         (cons (list ‘fork
                     (list ‘lambda
                           ‘()
                           (car tasks)))
               (make-forks (cdr tasks)))))

With this definition, what together returns for the example is the list

   (begin (fork (lambda () (counter 6)))
          (fork (lambda () (counter 201)))
          (waiter))

but that returned list isn’t printed as the final result; instead, the returned list is evaluated as a Scheme expression.

Although conceptually simple, this mechanism is messy because of the complexity of constructing the expression using cons and append. A more magical-seeming version, but one that’s easier to use, looks like this:

   (extend-syntax (together)
     ((together task ...)
      (begin (fork (lambda () task)) ... (waiter))))

This says that an expression beginning with the word together followed by zero or more subexpressions (the tasks) should be replaced by one with begin followed by invocations of fork with the lambda-ized tasks and then (waiter). It is possible, although complicated, to write an extend-syntax macro that translates this new notation into define-macro form.

The trouble with the kinds of macros presented so far is that they reintroduce the sort of name-capture bugs that are possible with dynamic scope but not with lexical scope. For example, suppose you say

   (define (times n)
     (lambda (x) (* x n)))
   
   (define (multiples x)
     (map (times x) ‘(1 2 3 4 5)))
   > (multiples 4)
   (4 8 12 16 20)

There is no conflict between the two variables named x in the two procedures; each has the value it’s supposed to have. Watch what happens if you redefine times as a macro this way:

   (define-macro (times exp)
     (list ‘lambda
           ‘(x)
           (list ‘*
                 ‘x
                 (cadr exp))))

or this way:

   (extend-syntax (times)
     ((times n)
      (lambda (x) (* x n))))

   > (multiples 4)
   (1 4 9 16 25)

The trouble is that the macro invocation (times x) is transformed into

   (lambda (x) (* x x))

The use of x in the macro has captured the intended use of x in the calling procedure.

A hygienic macro system is one that avoids these possible name captures by, in effect, invisibly renaming all the variables created within a macro invocation and renaming all the variables created within the macro expansion—the expression that replaces the macro invocation—to guarantee that all the names are unique. That is, the macro system reads this example as if it were written

   (extend-syntax (times)
     ((times n)
      (lambda (x1) (* x1 n))))

but the name shown here as x1 is actually a special name, tagged to indicate that it’s separate from any x elsewhere in the program.


Previous Table of Contents Next