Guile scheme's syntax parameters

One of the distinguishing features of the scheme programming language, when it was created in 1975, was its use of lexical scoping, which departed from the tradition of dynamic scoping of other lisps at the time.

1. Dynamic vs lexical scope

Since the release of scheme 50 years ago, lexical scoping has become the way (as far as I can tell) every language does things. For this reaseon, most reader will not know what either of these scoping rules mean and will see lexical scope as the obvious "correct" behaviour.

Dynamic and lexical scoping differ in how they treat free variables of lambdas. In the following code, the variabel c is free in the lambda as it is not bound in the parameter list.

(setq c "something")
(setq func (lambda (a b) c))

In dynamic binding, when the lambda is executed, the value of c will be retrieved from the binding closest to the call site. In lexical scoping, the value will be retrieved from the binding closest to the place where the lambda was created.

For instance, if the following code is executed in dynamic binding:

(setq x "initial-value")

(let ((some-function
       (let ((x "modified-value"))
         (lambda () x))))
  (message "the value is %s" (funcall some-function)))

We get

the value is initial-value

But if it is executed in lexical binding, we get

the value is modified-value

2. Scheme dynamic parameters

Even if history has decided lexical scope is a generally better paradigm, there are still cases where we would like to have something like special variables that are dynamically bound. For instance, we might want to temporarily set a global variable of a program to some value so that the "verbose" mode is activated for some specific part of the program.

old_verbose = VERBOSE
VERBOSE = True
do_something()
VERBOSE = old_verbose

This code, however is incorrect because if do_something throws an exception, the global variable is left with the "temporary" value. What you would want is actually:

old_verbose = VERBOSE
VERBOSE = True
try:
    do_something()
finally:
    VERBOSE = old_verbose

Scheme's parameters (constructed with make-parameter) work just like this.

(define verbose (make-parameter #f))

(define (do-something)
  (when (verbose)
    (format #t "verbose mode on\n")))


(format #t "a:\n")
(parameterize ((verbose 1))
  (do-something))

(format #t "b:\n")
(do-something)
a:
verbose mode on
b:

Parameters let us control explicitly where we want dynamic scope. They use special low level runtime facilities so that they interact well with re-entrant continuations, exceptions and all kinds of dynamic behaviour. They are used for things such as the default text encoding or the default output port.

3. lexical scope as hygiene

One way to conceptualise lexical scope is to think of it as hygiene for variables. Lexical scope acts as a form of access control. The free variables of a closure are inaccessible by the caller of the closure. This lets us build an object system using only these closures:

(use-modules (ice-9 match))

(define (make-person name age)
  (lambda msg
    (match msg
      (('get-name) name)
      (('get-age) age)
      (('set-name new-name)
       (set! name new-name))
      (('set-age new-age)
       (set! age new-age))
      (('say-hello)
       (format #t "my name is ~a and I am ~a years old\n"
               name age)))))


(define me (make-person "Sam" 12))

(me 'say-hello)
(me 'set-name "John")
(me 'say-hello)

Output:

my name is Sam and I am 12 years old
my name is John and I am 12 years old

4. Scheme hygienic macros

Another thing scheme pioneered was the use of hygienic macros for metaprogramming. Hygienic macros prevent variable references from being shadowed by binding created during macro expansion.


(define-syntax-rule (bind-x body)
  (let ((x 42))
    body))

(define x 0)

(bind-x (+ x 1))
1

Here, if there were no hygiene, this is what the last part would expand to:

(define x 0)

(bind-x (+ x 1))
(define x 0)

(let ((x 42))
  (+ x 1))

5. Syntax parameters

Syntactic parameters act a bit like normal parameters, but exist during macro expansion. Whereas parameterize changes the value of a pre-existing parameter for a limited time, syntax-parameterize changes the definition of a pre-existing macro for a limited time.

(define-syntax-parameter return
  (lambda (s)
    (syntax-violation 'return "return used outside `with-return`" s)))

(return 123)
ice-9/boot-9.scm:1685:16: In procedure raise-exception:
Syntax error:
unknown file:12:0: return: return used outside `with-return` in form (return 123)

Entering a new prompt.  Type `,bt' for a backtrace or `,q' to continue.
scheme@(guile-user) [1]> 
(define-syntax-parameter return
  (lambda (s)
    (syntax-violation 'return "return used outside `with-return`" s)))

(define-syntax-rule (with-return body body* ...)
  (call/cc (lambda (retfunc)
             (syntax-parameterize
                 ((return
                   (lambda (s) (syntax-case s () ((_ x) #'(retfunc x))))))
               body
               body* ...))))

(define val
  (with-return
   123
   (return 456)
   789))

(format #t "val: ~a\n" val)
val: 456

Guile scheme's syntax parameters let you create macros that modify the behaviour of other macros at expansion time and it turns out that you can actually use this same concept to have the macro expansion check the await keyword I defined in an old post lives within an async block instead of throwing an error at runtime.

Date: 2024-07-21 Sun 00:00

Author: Justin Veilleux

Created: 2025-09-25 Thu 18:11

Validate