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.