Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Functions With Free Variables: Poorman's Implicit Parameters #24

Open
shhyou opened this issue Sep 3, 2021 · 1 comment
Open

Functions With Free Variables: Poorman's Implicit Parameters #24

shhyou opened this issue Sep 3, 2021 · 1 comment

Comments

@shhyou
Copy link

shhyou commented Sep 3, 2021

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.

raise-who

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.

  1. The define/freevar form expands to a new definition storing the
    original code and a macro for binding the free identifiers.

  2. 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-term also has the implementation of the
    use-site macro and serves as the transformer in the expansion of
    for define/freevar.

  3. 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

@bennn
Copy link
Member

bennn commented Sep 15, 2021

thank you for these major contributions

bennn added a commit to syntax-objects/syntax-parse-example that referenced this issue Sep 21, 2021
bennn added a commit to syntax-objects/syntax-parse-example that referenced this issue Oct 27, 2021
bennn added a commit to syntax-objects/syntax-parse-example that referenced this issue Oct 27, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants