Macro for keyword and default values of function a

2019-07-25 07:48发布

问题:

Keyword and default arguments can be used in Racket functions as shown on this page: https://docs.racket-lang.org/guide/lambda.html

(define greet
  (lambda (#:hi [hi "Hello"] given #:last [surname "Smith"])
    (string-append hi ", " given " " surname)))

> (greet "John")
"Hello, John Smith"
> (greet "Karl" #:last "Marx")
"Hello, Karl Marx"
> (greet "John" #:hi "Howdy")
"Howdy, John Smith"
> (greet "Karl" #:last "Marx" #:hi "Guten Tag")
"Guten Tag, Karl Marx"

Since Racket is said to be able to create new language definitions easily, is it possible to create a macro so that functions can be defined as follows:

(define (greet2 (hi "hello") (given "Joe") (surname "Smith"))
    (string-append hi ", " given " " surname))

It should be possible to call the functions with arguments in any order as follows:

(greet2 (surname "Watchman") (hi "hi") (given "Robert") )

Just to clarify, following works:

(define (greet3 #:hi [hi "hello"] #:given  [given "Joe"] #:surname  [surname "Smith"])
    (string-append hi ", " given " " surname))

(greet3 #:surname "Watchman" #:hi "hey" #:given "Robert" )

But I want following to work (parentheses may be () or [] or even {} ):

(define (greet4 [hi "hello"]  [given "Joe"]  [surname "Smith"])
    (string-append hi ", " given " " surname))

(greet4 [surname "Watchman"] [hi "hey"] [given "Robert"])

Basically, I want to get rid of "#:surname" part (since it appears repetitive) to improve ease of typing.

How can such a macro be created? I attempted some code on the lines of:

(define-syntax-rule (myfn (arg1 val1) (arg2 val2)  ...)   
    (myfn #:arg1 val1 #:arg2 val2 ...))

but it does not work.

Thanks for your comments / answers.

Edit:

I modified the code from answer by @AlexKnauth to use {} instead of [] and this also works well:

(require syntax/parse/define ; for define-simple-macro
         (only-in racket [define old-define] [#%app old-#%app])
         (for-syntax syntax/stx)) ; for stx-map

(begin-for-syntax
  ;; identifier->keyword : Identifer -> (Syntaxof Keyword)
  (define (identifier->keyword id)
    (datum->syntax id (string->keyword (symbol->string (syntax-e id))) id id))
  ;; for use in define
  (define-syntax-class arg-spec
    [pattern name:id
             ;; a sequence of one thing
             #:with (norm ...) #'(name)]
    [pattern {name:id default-val:expr}
             #:when (equal? #\{ (syntax-property this-syntax 'paren-shape))
             #:with name-kw (identifier->keyword #'name)
             ;; a sequence of two things
             #:with (norm ...) #'(name-kw {name default-val})]))

(define-simple-macro (define (fn arg:arg-spec ...) body ...+)
  (old-define (fn arg.norm ... ...) body ...))

(begin-for-syntax
  ;; for use in #%app
  (define-syntax-class arg
    [pattern arg:expr
             #:when (not (equal? #\{ (syntax-property this-syntax 'paren-shape)))
             ;; a sequence of one thing
             #:with (norm ...) #'(arg)]
    [pattern {name:id arg:expr}
             #:when (equal? #\{ (syntax-property this-syntax 'paren-shape))
             #:with name-kw (identifier->keyword #'name)
             ;; a sequence of two things
             #:with (norm ...) #'(name-kw arg)]))

(require (for-syntax (only-in racket [#%app app])))

(define-simple-macro (#%app fn arg:arg ...)
  #:fail-when (app equal? #\{ (app syntax-property this-syntax 'paren-shape))
  "function applications can't use `{`"
  (old-#%app fn arg.norm ... ...))

Example usage:

> (define (greet5 hi  {given "Joe"}  {surname "Smith"})
    (string-append hi ", " given " " surname))
> (greet5 "Hey" {surname "Watchman"} {given "Robert"})
"Hey, Robert Watchman"

And there is flexibility of order of arguments:

> (greet5 {surname "Watchman"} "Howya" {given "Robert"})
"Howya, Robert Watchman"

Now simple define statements are not working:

(define x 0)
  define: bad syntax in: (define x 0)

Instead (old-define x 0) works.

回答1:

You can do this, but you'll need something slightly more complicated using define-simple-macro and an identifier->keyword helper function.

You can define your own define form and your own #%app to use for function applications, but to do that you need to expand into racket's old versions, so you need import renamed versions, using the only-in require form.

You'll also need to map the identifier->keyword function over all the identifiers. A useful function for that is stx-map from syntax/stx. It's similar to map, but it works on syntax objects as well.

#lang racket
(require syntax/parse/define ; for define-simple-macro
         (only-in racket [define old-define] [#%app old-#%app])
         (for-syntax syntax/stx)) ; for stx-map

To define a helper function for a macro to use to transform the syntax, you need to put it inside a begin-for-syntax

(begin-for-syntax
  ;; identifier->keyword : Identifer -> (Syntaxof Keyword)
  (define (identifier->keyword id)
    (datum->syntax id (string->keyword (symbol->string (syntax-e id))) id id)))

This answer defines two versions of this: one that supports only named arguments, and one that supports both named and positional arguments. However, both of them will use the identifier->keyword helper function.

Just named arguments

This new version of define takes the arg-names and transforms them into keywords using the identifier->keyword helper function, but since it needs to transform a syntax-list of them, it uses stx-map.

It then groups the keywords together with the [arg-name default-val] pairs to create sequences of arg-kw [arg-name default-val]. With concrete code, this will group the #:hi with the [hi "hello"] to create sequences of #:hi [hi "hello"] which is what the old define form expects.

(define-simple-macro (define (fn [arg-name default-val] ...) body ...+)
  ;; stx-map is like map, but for syntax lists
  #:with (arg-kw ...) (stx-map identifier->keyword #'(arg-name ...))
  ;; group the arg-kws and [arg-name default-val] pairs together as sequences
  #:with ((arg-kw/arg+default ...) ...) #'((arg-kw [arg-name default-val]) ...)
  ;; expand to old-define
  (old-define (fn arg-kw/arg+default ... ...) body ...))

This defines an #%app macro that will be inserted implicitly on all function applications. (f stuff ...) will expand into (#%app f stuff ...), so (greet4 [hi "hey"]) will expand into (#%app greet4 [hi "hey"]).

This macro transforms (#%app greet4 [hi "hey"]) into (old-#%app greet4 #:hi "hey").

(require (for-syntax (only-in racket [#%app app])))

(define-simple-macro (#%app fn [arg-name val] ...)
  ;; same stx-map as before, but need to use racket's `#%app`, renamed to `app` here, explicitly
  #:with (arg-kw ...) (app stx-map identifier->keyword #'(arg-name ...))
  ;; group the arg-kws and vals together as sequences
  #:with ((arg-kw/val ...) ...) #'((arg-kw val) ...)
  ;; expand to old-#%app
  (old-#%app fn arg-kw/val ... ...))

Using the new define form:

> (define (greet4 [hi "hello"]  [given "Joe"]  [surname "Smith"])
    ;; have to use old-#%app for this string-append call
    (old-#%app string-append hi ", " given " " surname))

These impliticly use the new #%app macro defined above:

> (greet4 [surname "Watchman"] [hi "hey"] [given "Robert"])
"hey, Robert Watchman"

Omitting arguments makes it use the default:

> (greet4 [hi "hey"] [given "Robert"])
"hey, Robert Smith"

And functions like greet4 can still be used within higher-order functions:

> (old-define display-greeting (old-#%app compose displayln greet4))
> (display-greeting [hi "hey"] [given "Robert"])
hey, Robert Smith

Both named and positional arguments

The macros above support only named arguments, so functions that use positional arguments instead can't be defined used with them. However, it's possible to support both positional arguments and named arguments in the same macro.

To do this, we would have to make square brackets [ and ] "special" so that define and #%app can tell between a named argument and an expression. To do that, we can use (syntax-property stx 'paren-shape), which will return the character #\[ if stx was written with square brackets.

So to specify a positional argument in a define, you would just use a normal identifier, and to use a named argument, you would use square brackets. So an argument specification could be either one of those variants. You can express that with a syntax class.

Since it is used by the macro to transform the syntax, it needs to be in a begin-for-syntax along with identifier->keyword:

(begin-for-syntax
  ;; identifier->keyword : Identifer -> (Syntaxof Keyword)
  (define (identifier->keyword id)
    (datum->syntax id (string->keyword (symbol->string (syntax-e id))) id id))
  ;; for use in define
  (define-syntax-class arg-spec
    [pattern name:id
             ;; a sequence of one thing
             #:with (norm ...) #'(name)]
    [pattern [name:id default-val:expr]
             #:when (equal? #\[ (syntax-property this-syntax 'paren-shape))
             #:with name-kw (identifier->keyword #'name)
             ;; a sequence of two things
             #:with (norm ...) #'(name-kw [name default-val])]))

And then you can define define like this, using arg:arg-spec to specify that arg uses the arg-spec syntax class.

(define-simple-macro (define (fn arg:arg-spec ...) body ...+)
  (old-define (fn arg.norm ... ...) body ...))

For a given arg, arg.norm ... is either a sequence of one thing (for positional arguments) or a sequence of two things (for named arguments). Then since arg itself can appear any number of times, arg.norm ... is under another ellipsis, so that arg.norm is under two ellipses.

The #%app macro will use a similar syntax class, but it will be slightly more complicated because the args can be arbitrary expressions, and it needs to make sure that normal expressions don't use square brackets.

Again, an argument has two variants. The first variant needs to be an expression that doesn't use square brackets, and the second variant needs to be a name and an expression wrapped in square brackets.

(begin-for-syntax
  ;; for use in #%app
  (define-syntax-class arg
    [pattern arg:expr
             #:when (not (equal? #\[ (syntax-property this-syntax 'paren-shape)))
             ;; a sequence of one thing
             #:with (norm ...) #'(arg)]
    [pattern [name:id arg:expr]
             #:when (equal? #\[ (syntax-property this-syntax 'paren-shape))
             #:with name-kw (identifier->keyword #'name)
             ;; a sequence of two things
             #:with (norm ...) #'(name-kw arg)]))

And the #%app macro itself needs to make sure that it's not used with square brackets. It can do that with a #:fail-when clause:

(require (for-syntax (only-in racket [#%app app])))

(define-simple-macro (#%app fn arg:arg ...)
  #:fail-when (app equal? #\[ (app syntax-property this-syntax 'paren-shape))
  "function applications can't use `[`"
  (old-#%app fn arg.norm ... ...))

Now greet4 can be defined using named arguments, but it can also use string-append with positional arguments.

> (define (greet4 [hi "hello"]  [given "Joe"]  [surname "Smith"])
    (string-append hi ", " given " " surname))
> (greet4 [surname "Watchman"] [hi "hey"] [given "Robert"])
"hey, Robert Watchman"

Just like before, omitting arguments causes it to use the default.

> (greet4 [hi "hey"] [given "Robert"])
"hey, Robert Smith"

What's different now is that positional arguments work,

> (displayln (string-append "FROGGY" "!"))
FROGGY!

And that square brackets [ and ] can't be used for expression any more.

> (displayln [string-append "FROGGY" "!"])
;#%app: expected arg
> [string-append "FROGGY" "!"]
;#%app: function applications can't use `[`

Just like before, greet4 can be used in higher-order functions like compose.

> (old-define display-greeting (compose displayln greet4))
> (display-greeting [hi "hey"] [given "Robert"])
hey, Robert Smith

Modifying it to support non-function definitions

The define macros above were specialized for function definitions, to keep it simple. However, you can support non-function definitions as well by using define-syntax-parser and specifying multiple cases.

The define-simple-macro definition here

(define-simple-macro (define (fn arg:arg-spec ...) body ...+)
  (old-define (fn arg.norm ... ...) body ...))

Is equivalent to using define-syntax-parser with one clause.

(define-syntax-parser define
  [(define (fn arg:arg-spec ...) body ...+)
   #'(old-define (fn arg.norm ... ...) body ...)])

So to support multiple clauses, you can write:

(define-syntax-parser define
  [(define x:id val:expr)
   #'(old-define x val)]
  [(define (fn arg:arg-spec ...) body ...+)
   #'(old-define (fn arg.norm ... ...) body ...)])

Then this will also support definitions like (define x 0).



标签: scheme racket