Why doesn't 'define' work in a macro?

155 views
Skip to first unread message

Zeta Convex

unread,
Jul 28, 2018, 2:28:32 AM7/28/18
to Racket Users
When I run the program:

#lang racket
(define-syntax make-a
  (syntax-rules ()
    ((make-a)
     (define a 1))))

(make-a)
(display a)

I get
a: unbound identifier in module in: a

Why is that, and how do I fix the macro?

I would have thought that (make-a) expands to (define a 1), so I am puzzled as to why it isn't working.

Philip McGrath

unread,
Jul 28, 2018, 2:39:37 AM7/28/18
to Zeta Convex, Racket Users
This is because Racket's macros are "hygienic" by default: they don't accidentally capture identifiers, and they aren't susceptible to accidental capture of identifiers they introduce in their expansions. The Racket Guide explains more: http://docs.racket-lang.org/guide/pattern-macros.html#%28part._.Lexical_.Scope%29

Hygiene is mandatory using `syntax-rules`, but other parts of Racket's macro system allow you to "break hygiene" and intentionally capture an identifier. For example:

#lang racket

(require syntax/parse/define)

(define-syntax-parser make-a
  [(_)
   #:with a (datum->syntax this-syntax 'a)
   #'(define a 1)])

(make-a)
(display a)



-Philip

--
You received this message because you are subscribed to the Google Groups "Racket Users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to racket-users+unsubscribe@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Ben Greenman

unread,
Jul 28, 2018, 1:07:43 PM7/28/18
to Philip McGrath, Zeta Convex, Racket Users
Instead of breaking hygience, the macro can accept an identifier:

#lang racket

(require syntax/parse/define)

(define-syntax-parser make-
  [(_ x)
   #'(define x 1)])

(make- a)
(display a)



or, with a syntax-rule:


#lang racket

(define-syntax-rule (make- x)
  (define x 1))

(make- a)
(display a)

Alexis King

unread,
Jul 29, 2018, 12:36:29 PM7/29/18
to Zeta Convex, Racket Users
Other people have answered why your macro doesn’t do what you want; the
answer is “hygiene”. However, what people haven’t really answered is
“why hygiene?”, since here you clearly seem to want a different
behavior. I’m going to make the case that the behavior Racket exhibits
is not only the correct one but is actually the intuitive one, if you
tweak your intuition slightly.

When we write definitions inside of functions, we expect those
definitions to be locally scoped. That is, when we write

(define (f x)
(define y (add1 x))
(* y 2))

we don’t expect `y` to be accessible outside of the body of `f`. If
defining `y` inside `f` did in some way affect the value of `y` outside
of `f`, programmers would probably be rather upset.

In many older programming languages, however, this notion of scope had
not yet been accepted. Instead of lexical scope, which we have today,
many programming languages used dynamic scope, which treated variables
more like registers, and using the same name for a variable would
temporarily overwrite its value globally. For example, given the
definitions

(define y 5)

(define (add-y n)
(+ n y))

(define (f x)
(define y 10)
(add-y x))

then (f 2) would be 12, not 7! A modern programmer would probably find
this completely absurd, and for good reason — lexical scope is critical
to being able to reason about a program in fragments. With lexical
scope, we can look at the definition of `add-y` above, locate the
definition of `y` in scope, and conclude that `add-y` is a function that
adds 5 to its argument. With dynamic scope, we don’t know what `add-y`
does without considering the scope of every single call site, and we can
no longer consider `add-y` to be interchangeable with “add 5”. In large
programs, this lack of local reasoning can make thinking about a
program’s behavior overwhelming.

Hopefully, this should sell you on the idea that lexical scope is a good
thing, since we want local reasoning. Therefore, we should also demand
lexically-scoped macros! Remember that the key idea of lexical scope is
that scope is syntactic: the bindings of identifiers can be determined
by examining the source text of the program surrounding the definition,
not by examining use sites and certainly not by running the program.

Racket’s syntax-rules implements lexically-scoped macros. References to
identifiers inside the body of the macro work just like references to
identifiers inside the body of a function, and definitions inside a
macro are likewise scoped to the code of that macro. This means that
even if we reimplemented the above example using macros, like this

(define y 5)

(define-syntax-rule (add-y n)
(+ n y))

(define (f x)
(define y 10)
(add-y x))

then (f 2) is still 7, as we expect, not 12. Just as this is critical
for reasoning about programs with functions, it’s similarly important
for reasoning about programs with macros.

The takeaway here is that hygiene, which refers to the system that
implements lexical scope for macros, is your friend, not your enemy, and
it’s the right default. Of course, Racket has ways to opt-in to dynamic
scope of runtime variables (via parameters), and similarly, it has ways
to opt-in to non-lexically scoped code generation, as Philip has already
mentioned.

Alexis

Matthias Felleisen

unread,
Jul 29, 2018, 12:45:02 PM7/29/18
to Alexis King, Zeta Convex, Racket Users

> On Jul 29, 2018, at 6:36 PM, Alexis King <lexi....@gmail.com> wrote:


Alexis’s post is right on, almost like what was in my brain when I borrowed the term ‘hygiene’ from a Barendregt paper on how to think about substitutions. But for accuracy, I’d like to point out that the following is a common misunderstanding:

> Racket has ways to opt-in to dynamic scope of runtime variables (via parameters),

Dynamic scope was a mistake of historical proportion in the Lisp branch of the PL family (with some influence on others). Nobody would want dynamic scope back, and Racket does not support it. What Racket supports is dynamic extent or as I prefer to call it dynamic (temporary) assignment. The difference may appear subtle to a novice, just like the radical difference between the idea of state in the US constitution and all European ones. At first you don’t see it, and later you can’t even imagine how the two could ever have been mistaken for one another.

See Common Lisp 2 for details. Guy’s write-up is unparalleled.

— Matthias




Alexis King

unread,
Jul 29, 2018, 12:56:37 PM7/29/18
to Matthias Felleisen, Racket Users
> On Jul 29, 2018, at 11:44, Matthias Felleisen <matt...@felleisen.org>
> wrote:
>
>> Racket has ways to opt-in to dynamic scope of runtime variables (via parameters),
>
> Dynamic scope was a mistake of historical proportion in the Lisp
> branch of the PL family (with some influence on others). Nobody would
> want dynamic scope back, and Racket does not support it. What Racket
> supports is dynamic extent or as I prefer to call it dynamic
> (temporary) assignment.

I’ll be honest, I’ve heard this before, and I’ve never understood the
difference. Taking a look at Common Lisp, which claims to have dynamic
scope for special variables, we can reproduce the program in my email:

(defvar y 5)

(defun add-y (n)
(+ n y))

(defun f (x)
(let ((y 10))
(add-y x)))

Now, evaluating (f 2) produces 12, but following that by evaluating
(add-y 2) produces 7. This is precisely equivalent to Racket’s behavior
if y is made a parameter and the let is replaced with parameterize — the
value of y is adjusted only within the dynamic extent of the let block,
and afterwards, it is reverted.

Is it possible that this is, from your point of view, not dynamic scope,
and therefore I have just never used a system that implements “true”
dynamic scope? Or is there some distinction between the behavior of CL
special variables and Racket parameters that I do not understand?

Alexis King

unread,
Jul 29, 2018, 2:11:26 PM7/29/18
to Racket Users, Matthias Felleisen
An little over an hour later, I’ll now try and answer my own question.
After doing some digging, I think Matthias’s reference to Common Lisp
the Language, 2nd Edition holds the answer I was looking for. It draws
the line between scope and extent as follows:

> Scope refers to the spatial or textual region of the program within
> which references may occur. Extent refers to the interval of time
> during which references may occur.

It goes on to define “dynamic scope” as the combination of indefinite
scope and dynamic extent, where “indefinite scope” means “references may
occur anywhere, in any program” and dynamic extent is the property I
described in my original email. In that sense, both Racket parameters
and CL special variables have dynamic extent, but Racket parameters have
lexical scope, and CL special variables have indefinite scope.

I am not a Common Lisper, but my understanding of indefinite scope is
effectively that a binding is determined entirely symbolically, and that
binding is global. That is, if a binding named `foo` is declared in one
part of the program, a completely different part of the program can
reference the same binding using the name `foo`, whether or not it
imports the original binding site. In contrast, in Racket, parameters
are still bound to lexical variables, so to actually get at the value
of a parameter, its lexical binding must be explicitly imported.

My understanding is that, in practice, the impacts of indefinite scope
are mitigated in CL by the existence of package-namespaced symbols, so
bindings with the same symbolic name defined in different packages will
not actually conflict unless the namespace is explicitly specified to
be the same. Racket has no such notion, however, so indefinite scope in
Racket would surely be disastrous.

In practice, I’m not wholly convinced that these definitions of scope
and extent are universal, and likewise, I’m not completely convinced
that the phrase “dynamic scope” as understood by most people does not
apply to Racket’s parameters. But at least I think I now understand
Matthias’s objection to that characterization. :)

Shu-Hung You

unread,
Jul 29, 2018, 2:14:41 PM7/29/18
to Alexis King, Racket Users
This program shows one difference (in elisp):

(defvar add-y
(let ((y 5))
(lambda (n) (+ n y))))

(defun f (x)
(let ((y 10))
(funcall add-y x)))

(f 5)
;=> 15

(funcall add-y 5)
;=> Lisp error: (void-variable y)

In contract, parameters are defined and accessed *lexically* though
their values could be changed during execution, and the parameterize
form implements the semantics of 'changing values during the dynamic
extent of the body'.

#lang racket

(define y (make-parameter 'no-lexical-reference-at-all))

(define add-y
(let ([y (make-parameter -99)])
(parameterize ([y 5])
(λ (n) (+ n (y))))))

(define (f x)
(let ([y (make-parameter 0)])
(parameterize ([y 10])
(add-y x))))

(f 5)
;=> -94
> --
> You received this message because you are subscribed to the Google Groups "Racket Users" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to racket-users...@googlegroups.com.

George Neuner

unread,
Jul 30, 2018, 1:01:23 AM7/30/18
to racket...@googlegroups.com
Leaving aside the question of what is meant by "dynamic scope",
there are a number of differences between Lisp's special vars and
Racket's parameters as I understand them.


Within their package (module), special vars are global - if you change
the value, it is visible everywhere within the package: e.g., to
functions being executed by concurrent threads.

Special vars behave as a stack for which only the top value is visible
at any given time. The "scope" of a special var is unlimited, global,
but the "extent" of any of its values is dynamic.

Also, special vars have another behavior depending on whether they are
declared using defvar or defparameter or defconstant. The *current*
value of a defvar variable is maintained if the program is executed
again without the defining package being reloaded. The value of a
defparameter is reset with every execution. The value of a
defconstant only can be initialized and can't be changed afterward.


A Racket parameter is a lexically scoped object - it can be declared
at the top level, but need not be.

Parameters do behave as a stack: their value can be changed for a
particular extent and it reverts to the previous value upon leaving
that extent. So the values have dynamic extent, but the scope of any
particular parameter value is lexical and limited.


So, for example, two threads in different scopes - but in the same
package - could communicate through the globally available special
var.

But the same is not true of a parameter. Two threads could share the
value of a parameter that existed before they began, but changing the
value of the parameter in one thread does not change it for the other.
[you could make it work with coroutines, but not with threads.]


Obviously, I may be misunderstanding something, but I don't see
special vars and parameters as being the same or even very similar.

George

Hendrik Boom

unread,
Jul 30, 2018, 1:04:42 PM7/30/18
to racket...@googlegroups.com
How do parameter values behave with continuations?

Or, it the same 'return address' is executed a second time, does the
parameter revert to the stack it possessed at the first return?

i.e., is the behaviour of parameters syncronised with the so-called "stack"

(yes, I realise that's a misnomer in the face of massive continuation
scrambling; the "stack" becomes just as scrambled as the continuations.
I'm asking whether the stack of parameter values is scrambled
isomorphically to the return "stack")

-- hendrik

>
>
> So, for example, two threads in different scopes - but in the same
> package - could communicate through the globally available special
> var.
>
> But the same is not true of a parameter. Two threads could share the
> value of a parameter that existed before they began, but changing the
> value of the parameter in one thread does not change it for the other.
> [you could make it work with coroutines, but not with threads.]
>
>
> Obviously, I may be misunderstanding something, but I don't see
> special vars and parameters as being the same or even very similar.
>
> George
>

John Clements

unread,
Jul 30, 2018, 1:43:50 PM7/30/18
to Hendrik Boom, racket...@googlegroups.com
To a first approximation, the answer to this should be “yes”. Here’s
some sample code that might convince you of this.

#lang racket

(define z (make-parameter #f))

(define saved-kont #f)
(define counter 3)

(let/ec abort
(parameterize ([z #t])
(let/cc kont
(set! saved-kont kont))
(printf "inside, value of z is: ~v\n" (z)))
(printf "outside, value of z is now: ~v\n" (z))
;; let's do it again:
(set! counter (- counter 1))
(when (< 0 counter)
(saved-kont)))

… with output:

inside, value of z is: #t
outside, value of z is now: #f
inside, value of z is: #t
outside, value of z is now: #f
inside, value of z is: #t
outside, value of z is now: #f


Last time I checked, the parameter values are associated with the continuation using “continuation marks”. You can definitely think of this as associating values with a stack frame in such a way that jumping back into a continuation restores its parameter values.

Also, I believe there’s a way to break these rules, with the various “current-parameterization” forms.

John

Hendrik Boom

unread,
Jul 30, 2018, 1:57:33 PM7/30/18
to racket...@googlegroups.com
On Mon, Jul 30, 2018 at 01:43:47PM -0400, John Clements wrote:
>
>
> > On Jul 30, 2018, at 13:04, Hendrik Boom <hen...@topoi.pooq.com> wrote:

> >
> > How do parameter values behave with continuations?
> >
> > Or, it the same 'return address' is executed a second time, does the
> > parameter revert to the stack it possessed at the first return?
> >
> > i.e., is the behaviour of parameters syncronised with the so-called "stack"
> >
> > (yes, I realise that's a misnomer in the face of massive continuation
> > scrambling; the "stack" becomes just as scrambled as the continuations.
> > I'm asking whether the stack of parameter values is scrambled
> > isomorphically to the return "stack”)
>
> To a first approximation, the answer to this should be “yes”. Here’s
> some sample code that might convince you of this.

Excellent!

-- hendrik
Reply all
Reply to author
Forward
0 new messages