Hi, Racket Discourse.
I am curious about how one introduces a new binding into a definition context--if I am using these terms correctly.
I am looking at defining out-parameters
in Racket, but my method of introducing these identifiers is crude, to say the least, and its scoping can be somewhat confusing due to its arbitrariness.
Basically, I want to introduce an identifier binding as if it were done via define-*
but without strictly using a define-*
form in the immediate context.
As an example:
(let ([exists? (my-out-proc 'input #out result))])
;; | ^^^^^^ value depends on whether it `exists?` or not
;; ^^^^^^^^^^^^ returns true or false
(if exists? (do-something result) #false))
In this piece of code, I would like to introduce the result
identifier as if it were defined normally, but obviously because the context of the surrounding call can be arbitrary (other than a let
, say), I don't want to explicitly introduce a define
because where would it go, practically speaking?
If you use this expression inside another procedure call, e.g.
(another-proc (my-out-proc 'input #out result))
...
(do-something result)
I would expect the result
to still work, which of course becomes tricky once you have to start traversing which contexts it belongs to and so on, in an ad hoc manner.
I have stumbled more or less vaguely onto this thread, but honestly, it is so unfamiliar that I am not sure how to take the information and run with it.
I think I understand the behavior you'd like to implement, but unfortunately I think that this would be quite difficult to achieve with Racket's expansion process.
Generally a macro cannot control the expansion of syntax surrounding it, so my-out-proc
wouldn't have a way of rewriting the surrounding code to insert the definitions. syntax-local-lift-expression
is one way to lift a definition into the surrounding context, but it generates a fresh name for the definition. So that operation is useful to lift the computation of an expression in order to avoid re-computing it, but it is not useful for making the definition of a name visible in a wider scope.
In order to transform the surrounding context, you would need to have a macro call around it. So let's imagine your sample code is wrapped with another macro you define, say with-out-parameters
:
(with-out-parameters
(let ([exists? (my-out-proc 'input #out result))])
(if exists? (do-something result) #false)))
The with-out-parameters
macro could potentially use local-expand
to partially expand the syntax within, transform it, and then continue expansion. You could in principle have a #lang racket/out
that is just like Racket, but replaces #%module-begin
with such a with-out-parameters
that knows how to use local-expand
and traverse the entire Racket core syntax in order to perform the transformation. The implementations of #%printing-module-begin
or splicing-syntax-parameterize
could provide some hints here. This is a quite complicated kind of macro! I'm not totally sure it would work for your objective.
The above assumes that you want out-parameters to work with general, macro-extensible Racket code. The problem is of course much simpler if instead you want to make your own restricted, non-extensible syntax with basic features like let, function application, etc where you apply this transformation.
1 Like
Hi, @michaelballantyne. Interesting, thank you for the thoughtful reply, the references to splicing-syntax-parameterize
, and #%printing-module-begin
are good--the former never even crossed my mind, and the latter I am unfamiliar with.
A similar thought crossed my mind, specifically regarding creating either:
- Anchors, like you do for namespaces when
eval
-ing code, in the sense that you create a free-standing link to an "out-context" which can then be used as needed, or
- As you mention, a scoping mechanism which achieves the desired effect explicitly, like with
parameterize-*
and friends, by wrapping the context.
I am willing to admit that perhaps these two cases are essentially the same, only slightly different in their usage, if you think about defining a parameter and then using it later to create a separate context.
I would have preferred it to be "plug-n-play" and work cleanly with mostly arbitrary code, but perhaps I am saddling up a horse that should be parked in the field a bit.
I am reminded of the define-new-top
macro in @Eutro's cadnr
sources.
Let's say you make a macro, (define-out-parameter-context ...)
that allows you declare which forms are out-parameter contexts. The scoping mechanism itself is simple, since we are already leaning on syntax-parameterize
and parameterize
.
It would only need a way to tag certain expressions in the body of the definition as these contexts. In other words, segregate them from one another.
It sort of works that way already, since I overrode the define
and lambda
syntaxes to detect out-parameters in their formals and accordingly wrap the body.
This avoids one having to be prescient about usage at the cost of some "boilerplate" (it's C#
all over again
).
Much obliged.