diff --git a/fresh-variable/fresh-variable-test.rkt b/fresh-variable/fresh-variable-test.rkt new file mode 100644 index 0000000..05bbdba --- /dev/null +++ b/fresh-variable/fresh-variable-test.rkt @@ -0,0 +1,50 @@ +#lang racket/base +(module+ test + (require rackunit syntax/macro-testing + (for-syntax racket/base syntax/parse syntax/transformer syntax-parse-example/fresh-variable/fresh-variable)) + + (define-syntax (define/immutable-parameter stx) + (syntax-parse stx + [(_ (name:id (~or* (~and _:id arg:fresh-variable) + [(~and _:id arg:fresh-variable) default-value:expr]) + ...) + body:expr ...+) + #'(define (name (~? [arg.fresh-var default-value] + arg.fresh-var) ...) + ;; disable set! on arg + (define-syntax arg + (make-variable-like-transformer #'arg.fresh-var #f)) + ... + body ...)])) + + (check-exn #rx"set!: cannot mutate identifier" + (lambda () + (convert-compile-time-error + (let () + (define/immutable-parameter (bad-fn n) + (set! n 12345) ;=> syntax error + (void)) + (void))))) + + (check-exn #rx"set!: cannot mutate identifier" + (lambda () + (convert-compile-time-error + (let () + (define/immutable-parameter (bad-fn-optional n [verbose? #f]) + (set! verbose? 12345) ;=> syntax error + (void)) + (void))))) + + (define/immutable-parameter (fib n [verbose? #f]) + (when verbose? + (printf "(fib ~a)\n" n)) + (cond + [(<= n 1) n] + [else + (+ (fib (- n 1) verbose?) + (fib (- n 2) verbose?))])) + + (check-equal? (fib 4) 3) + (check-equal? (fib 8) 21) + +) diff --git a/fresh-variable/fresh-variable.rkt b/fresh-variable/fresh-variable.rkt new file mode 100644 index 0000000..f01a7d0 --- /dev/null +++ b/fresh-variable/fresh-variable.rkt @@ -0,0 +1,13 @@ +#lang racket/base + +(provide fresh-variable) + +(require racket/syntax syntax/parse) + +(define-syntax-class (fresh-variable [context #f]) + #:attributes (fresh-var) + (pattern name + #:with temp-var (generate-temporary #'name) + #:with fresh-var (if context + (format-id context "~a" #'temp-var) + #'temp-var))) diff --git a/fresh-variable/fresh-variable.scrbl b/fresh-variable/fresh-variable.scrbl new file mode 100644 index 0000000..cf1204e --- /dev/null +++ b/fresh-variable/fresh-variable.scrbl @@ -0,0 +1,81 @@ +#lang syntax-parse-example +@require[ + (for-label racket/base racket/syntax syntax/transformer syntax/parse syntax-parse-example/fresh-variable/fresh-variable)] + +@(define fresh-variable-eval + (make-base-eval '(require (for-syntax racket/base syntax/parse syntax/transformer syntax-parse-example/fresh-variable/fresh-variable)))) + +@title{Generate Temporaries On The Fly: @tt{fresh-variable} Syntax Class} +@stxbee2021["shhyou" 22] + +@; ============================================================================= + +@defmodule[syntax-parse-example/fresh-variable/fresh-variable]{} + +Some macros need to generate a sequence of fresh identifiers corresponding to a +list of input forms. The standard solution is to invoke @racket[generate-temporaries] +with a syntax list and bind the result to a new pattern variable. However, the +new pattern variable is disconnected from the input forms and such an approach +quickly becomes unmanageable when the input forms come nested in more than one +ellipses. + +The @racket[fresh-variable] syntax class solves both these issues. +First, it tightly couples generated identifiers to the input forms. +The new identifiers even have DrRacket binding arrows. +Second, it leverages the @racket[syntax-parse] pattern matcher to handle +deeply-nested repetitions. + +@defidform[fresh-variable]{ + Syntax class that binds an attribute @racket[_fresh-var] to a fresh temporary + variable. + + In the example below, we create a macro @racket[define/immutable-parameter] + for defining functions whose parameters cannot be mutated by the function + body. + The macro parses arguments using the @racket[fresh-variable] syntax class + to generate temporary identifiers on the fly. + + @examples[#:eval fresh-variable-eval + (define-syntax (define/immutable-parameter stx) + (syntax-parse stx + [(_ (name:id (~or* (~and _:id arg:fresh-variable) + [(~and _:id arg:fresh-variable) default-value:expr]) + ...) + body:expr ...+) + #'(define (name (~? [arg.fresh-var default-value] + arg.fresh-var) ...) + ;; disable set! on arg + (define-syntax arg + (make-variable-like-transformer #'arg.fresh-var #f)) + ... + body ...)])) + + (define/immutable-parameter (fib n [verbose? #f]) + (code:comment "(set! n 12345) ;=> syntax error") + (when verbose? + (printf "(fib ~a)\n" n)) + (cond + [(<= n 1) n] + [else + (+ (fib (- n 1) verbose?) + (fib (- n 2) verbose?))])) + + (fib 5) + ] + + The implementation accepts any expression and generates a temporary identifier. + + @racketfile{fresh-variable.rkt} + + @itemize[ + @item{ + @bold{Q.} Why use @racket[_name] instead of asking for an identifier with @racket[_name:id]? + } + @item{ + @bold{A.} Some macros may let-bind subforms to first evaluate them for + later use. Therefore the subforms can be any expressions. I couldn't find + a way to pass syntax classes around or compose them, so the @racket[_:id] + specification is left out of the syntax class. + } + ] +} diff --git a/index.scrbl b/index.scrbl index 0219dcb..a5ad43b 100644 --- a/index.scrbl +++ b/index.scrbl @@ -44,3 +44,4 @@ @include-example{js-dict} @include-example{define-freevar} @include-example{fnarg} +@include-example{fresh-variable}