diff --git a/fnarg/fnarg-test.rkt b/fnarg/fnarg-test.rkt new file mode 100644 index 0000000..20765b8 --- /dev/null +++ b/fnarg/fnarg-test.rkt @@ -0,0 +1,58 @@ +#lang racket/base +(module+ test + (require rackunit syntax/macro-testing + (for-syntax racket/base syntax/parse syntax/transformer syntax-parse-example/fnarg/fnarg)) + + ;; macro for defining functions that cannot mutate their parameters and do + ;; not accept keyword arguments + (define-syntax (define/immutable-parameter stx) + (syntax-parse stx + [(_ (name:id (~var arg (fnarg #:keyword? #f)) ...) body:expr ...+) + ;; The `~@` form _splices_ the keyword at (A), if it exists, into the + ;; function header. + ;; The `~?` form at (B) chooses the first form if both the arg.fresh-var + ;; and arg.default attributes have values. Otherwise, `~?` chooses the + ;; second form. + #'(define (name (~@ (~? arg.keyword) ;; (A) + (~? [arg.fresh-var arg.default] ;; (B) + arg.fresh-var)) + ...) + ;; Disable set! for arg + (define-syntax arg.name + (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 x) + (set! x 'oops) + (void)) + (void))))) + + (check-exn #rx"keyword argument is not allowed" + (lambda () + (convert-compile-time-error + (define/immutable-parameter (has-kw a #:cache cache?) + (void))))) + + (test-case "fib/cache" + (define fib-cache (make-hash)) + (define/immutable-parameter (fib n [cache #f]) + (cond + [(<= n 1) n] + [(and cache (hash-has-key? cache n)) + (hash-ref cache n)] + [else + (define result + (+ (fib (- n 1) cache) + (fib (- n 2) cache))) + (when cache + (hash-set! cache n result)) + result])) + + (check-equal? (fib 8) 21) + (check-equal? (fib 40 fib-cache) 102334155)) +) diff --git a/fnarg/fnarg.rkt b/fnarg/fnarg.rkt new file mode 100644 index 0000000..9b73b3e --- /dev/null +++ b/fnarg/fnarg.rkt @@ -0,0 +1,28 @@ +#lang racket/base + +(provide fnarg) + +(require racket/syntax syntax/parse) + +;; Use _splicing_ to parse a sequence of elements for keyword args. +(define-splicing-syntax-class (fnarg [allow-optional? #t] + #:keyword? [allow-keyword? #t]) + #:attributes (name fresh-var keyword default) + #:commit + (pattern name:id + #:attr keyword #f + #:attr default #f + #:with fresh-var (generate-temporary #'name)) + (pattern [name:id default:expr] + #:fail-unless allow-optional? "optional argument is not allowed" + #:attr keyword #f + #:with fresh-var (generate-temporary #'name)) + ;; The ~seq pattern describes a _sequence_ of elements + (pattern (~seq keyword:keyword name:id) + #:fail-unless allow-keyword? "keyword argument is not allowed" + #:attr default #f + #:with fresh-var (generate-temporary #'name)) + (pattern (~seq keyword:keyword [name:id default:expr]) + #:fail-unless allow-optional? "optional argument is not allowed" + #:fail-unless allow-keyword? "keyword argument is not allowed" + #:with fresh-var (generate-temporary #'name))) diff --git a/fnarg/fnarg.scrbl b/fnarg/fnarg.scrbl new file mode 100644 index 0000000..eadff8b --- /dev/null +++ b/fnarg/fnarg.scrbl @@ -0,0 +1,31 @@ +#lang syntax-parse-example +@require[ + (for-label racket/base syntax/parse syntax-parse-example/fnarg/fnarg)] + +@title{Function Parameter Syntax Class} +@stxbee2021["shhyou" 23] + +@; ============================================================================= + +@defmodule[syntax-parse-example/fnarg/fnarg]{} + +Syntax classes offer a powerful mechanism for abstracting over classes of +conceptually related patterns. Moreover, syntax classes can be parameterized, +with @racket[expr/c] being one prominent example. + +In this example, we define a syntax class that roughly captures the grammar of +formal parameters in function headers. It parses the name of the formal +parameter, the default expressions of optional arguments and the keywords of +keyworded arguments. + +@defidform[fnarg]{ + The @racket[fnarg] syntax class matches the basic argument patterns that + can appear in function headers: mandatory, optional, and keyword arguments. + The class optionally accepts two parameters to toggle whether optional and/or + keyword arguments are allowed. + + Refer to the source file @tt{fnarg-test.rkt} for an example use. + + @racketfile{fnarg.rkt} + +} diff --git a/index.scrbl b/index.scrbl index 30350b9..0219dcb 100644 --- a/index.scrbl +++ b/index.scrbl @@ -43,3 +43,4 @@ @include-example{flaggable-app} @include-example{js-dict} @include-example{define-freevar} +@include-example{fnarg}