Macro
Overview
In this example, the define/freevar macro introduces function
definitions with free variables in their body.
The free variables are resolved non-hygienically to any
bindings of an equal symbol name at each use site.
(define/freevar (function-id arg-id ...) #:freevars (freevar1-id freevar2-id ...) body1-expr body2-expr ...)
In conjunction with define/freevars, the with-freevar macro
locally renames the free variables for definitions introduced
using define/freevars.
(with-freevar function-id ([freevar-id new-freevar-id] ...) body-expr1 body-expr2 ...)
There is also the define counterpart of with-freevar:
(define/with-freevar new-function-id old-function-id
[freevar-id new-freevar-id]
...)Idea
The idea is transforming the original definition into a lambda function
that accepts the free variables and generating a new macro
which inserts the unhygienic references for the free variables at each
use site.
Here is an example illustrating the idea. The function raise-who-error
raises a syntax error and uses whichever binding named who
available as the name of the error message.
(define/freevar (raise-who-error message source-stx) #:freevars (who) (raise-syntax-error who message source-stx)) (let ([who 'knock-knock]) (raise-who-error "who's there" #'door))
Conceptually, thedefine/freevar form expands into a new definition
having the original code and a new macro for generating references of
the free variables:
(define (raise-who-error/impl who message source-stx) (raise-syntax-error who message source-stx)) (define-syntax (raise-who-error stx) (syntax-parse stx [(proc-src:id args ...) #:with who/use-site (syntax-property (format-id stx "~a" 'who #:source #'proc-src) 'original-for-check-syntax #t) (syntax/loc stx (raise-who-error/impl who/use-site args ...))]))
The new macro raise-who-error creates a reference, who/use-site,
to be captured non-hygienically using the context from the use site.
The expansion then proceeds with the use-site reference and calls
the original code.
Additionally, the use-site references have the source location of the
proc-src and the syntax property 'original-for-check-syntax
so Check Syntax and DrRacket can draw the binding arrows.
Caveat: mutation on the free variables will
not reflect on the original binding. Such a restriction can be overcome
using set!-transformers. The macro define/freevar can also disallow
mutation using make-variable-like-transformer.
Implementation
While the idea is straightforward, a direct translation generates a large
amount of code duplication. In the output of define/freevar, the only
varying parts are the names of the free variables and the identifier
of the actual implementation. The implementation of define/freevar
thus follows a common pattern in Racket to share the transformer code.
-
The
define/freevarform expands to a new definition storing the
original code and a macro for binding the free identifiers. -
The implementation introduces an applicative struct,
open-term,
that holds the list of free variables and the identifier of
the actual code.Being applicative,
open-termalso has the implementation of the
use-site macro and serves as the transformer in the expansion of
fordefine/freevar. -
When the macro expander calls an instance of
open-term, it extracts
names of the free variables and redirects the reference to the
actual code.
The idea behind custom pattern expanders and syntax class aliases are related: using structs to store varying information while attaching struct type properties to assign behavior.
#lang racket/base (require (for-syntax racket/base racket/list racket/syntax syntax/parse)) (provide define/freevar with-freevar define/with-freevar) (define-syntax (define/freevar stx) (syntax-parse stx [(_ (name:id arg:id ...) #:freevars (fv:id ...+) (~optional (~and #:immediate immediate-flag)) body:expr ...+) #:attr dup-id (or (check-duplicate-identifier (syntax-e #'(fv ... arg ...))) (cdr (check-duplicates (map cons (syntax->datum #'(fv ...)) (syntax-e #'(fv ...))) #:key car #:default '(#f . #f)))) #:do [(when (attribute dup-id) (raise-syntax-error 'define/freevar "duplicated argument or free variable name" stx (attribute dup-id)))] #:with name-with-fvs (format-id #'fresh-stx "~a/fvs" #'name) #:with immediate? (if (attribute immediate-flag) #t #f) #`(begin (define name-with-fvs #,(cond [(attribute immediate-flag) #`(λ (fv ...) (let ([name #,(syntax/loc stx (λ (arg ...) body ...))]) name))] [else #`(let ([name #,(syntax/loc stx (λ (fv ... arg ...) body ...))]) name)])) (define-syntax name (open-term #'name-with-fvs '(fv ...) '(arg ...) 'immediate?)))])) (define-syntax (with-freevar stx) (syntax-parse stx [(_ term-with-fv:id ([fv:id new-fv:id] ...) body:expr ...+) (syntax-property (syntax/loc stx (let-syntax ([term-with-fv (open-term-set-freevars 'with-freevar #'term-with-fv (hash (~@ 'fv 'new-fv) ...))]) body ...)) 'disappeared-use (list (syntax-local-introduce #'term-with-fv)))])) (define-syntax (define/with-freevar stx) (syntax-parse stx [(_ new-name:id original-term-with-fv:id [fv:id new-fv:id] ...) (syntax-property (syntax/loc stx (define-syntax new-name (open-term-set-freevars 'with-freevar #'original-term-with-fv (hash (~@ 'fv 'new-fv) ...)))) 'disappeared-use (list (syntax-local-introduce #'original-term-with-fv)))]))
The open-term itself can be used as a transformer, with the list of free
variables and the target identifier differs in different instances:
(begin-for-syntax (struct open-term (proc-stx freevars-name args-name immediate?) #:property prop:procedure (λ (self stx) (link-freevars self stx))) (define (freevars-in-context fvs #:context ctxt #:source src) (for/list ([fv (in-list fvs)]) (syntax-property (format-id ctxt "~a" fv #:source src) 'original-for-check-syntax #t))) (define (link-freevars self stx) (define/syntax-parse target (open-term-proc-stx self)) (syntax-parse stx [proc-src:id #:with (fv ...) (freevars-in-context (open-term-freevars-name self) #:context stx #:source #'proc-src) #:with (arg ...) (generate-temporaries (open-term-args-name self)) (cond [(open-term-immediate? self) (fix-app stx (syntax/loc stx (target fv ...)))] [else (quasisyntax/loc stx (λ (arg ...) #,(fix-app stx (syntax/loc stx (target fv ... arg ...)))))])] [(proc-src:id . args) #:with (fv ...) (freevars-in-context (open-term-freevars-name self) #:context stx #:source #'proc-src) (cond [(open-term-immediate? self) (fix-app stx (quasisyntax/loc stx (#,(fix-app stx (syntax/loc stx (target fv ...))) . args)))] [else (fix-app stx (syntax/loc stx (target fv ... . args)))])])) (define (fix-app ctxt app-stx) (define app-datum (syntax-e app-stx)) (datum->syntax ctxt app-datum app-stx app-stx)) (define (open-term-set-freevars who open-term-id map) (define (fail) (raise-syntax-error who "the binding is not defined by define/freevar" open-term-id)) (define self (syntax-local-value open-term-id fail)) (unless (open-term? self) (fail)) (define original-fvs (open-term-freevars-name self)) (define new-fvs (for/list ([fv (in-list original-fvs)]) (hash-ref map fv (λ () fv)))) (open-term (open-term-proc-stx self) new-fvs (open-term-args-name self) (open-term-immediate? self))))
Example
In this example, we define a function for computing the Fibonacci
sequence where the base values are left open and resolved at each
use site.
To illustrate the syntax, fib uses the option #:immediate that
immediately retrieve the value of init0 and init1 instead of
wrapping the identifier reference fib at X in a function.
(define/freevar (fib n) #:freevars (init0 init1) #:immediate (for/fold ([a init0] [b init1] [fib-list '()] #:result (reverse fib-list)) ([i (in-range n)]) (values b (+ a b) (cons a fib-list)))) (define init0 2) ;; X (let ([init1 13]) fib) ;; <- The #:immediate flag makes a difference ;; init0 shadows the global definition ;;=> '(0 1 1 2 3 5 8 ...) (let ([init0 0] [init1 1]) (fib 10)) ;; The free variable init1 is renamed to b (with-freevar fib ([init1 b]) (define b 4) (fib 10)) ;; Another renaming example. Free variables do not have bindings. (let ([b 5]) (with-freevar fib ([init1 b]) (fib 10))) ;; Define a new open term, fib-same, with free variables renamed from fib. (define/with-freevar fib-same fib [init0 S] [init1 S]) (let ([S 3]) (fib-same 10))
For the interested readers, the motivating example of define/freevar
is the following utility function for Redex:
#lang racket/base (require racket/pretty redex/reduction-semantics) (provide apply-reduction-relation*-->) (define/freevar (apply-reduction-relation*--> term) #:freevars (-->R) (pretty-print term) (for/fold ([term-list (list (list #f term))]) ([step (in-naturals)] #:break (null? term-list)) (define new-terms (apply-reduction-relation/tag-with-names -->R (list-ref (car term-list) 1))) (pretty-print new-terms) new-terms))
Licence
I license the code in this issue under the same MIT License that the Racket language uses and the texts under the Creative Commons Attribution 4.0 International License
