Racket 中函数参数的关键字和默认值的宏

Macro for keyword and default values of function arguments in Racket

可以在 Racket 函数中使用关键字和默认参数,如本页所示: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"

据说Racket可以很容易地创建新的语言定义,是否可以创建一个宏,这样函数就可以定义如下:

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

应该可以按以下任意顺序调用带有参数的函数:

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

澄清一下,以下作品:

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

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

但我希望以下内容起作用(括号可以是 () 或 [] 甚至是 {} ):

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

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

基本上,我想去掉“#:surname”部分(因为它看起来重复)以提高打字的便利性。

如何创建这样的宏?我尝试了一些代码:

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

但它不起作用。

感谢您的评论/回答。

编辑:

我修改了@AlexKnauth 回答的代码以使用 {} 而不是 [],这也很有效:

(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 ... ...))

用法示例:

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

并且参数顺序灵活:

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

现在简单的定义语句不起作用:

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

相反 (old-define x 0) 有效。

您可以这样做,但是您需要使用 define-simple-macroidentifier->keyword 辅助函数来稍微复杂一些。

你可以定义你自己的 define 形式和你自己的 #%app 用于功能应用,但是要做到这一点你需要扩展到球拍的旧版本,所以你需要导入重命名的版本,使用 only-in 要求形式。

您还需要将 identifier->keyword 函数映射到所有标识符上。一个有用的函数是 syntax/stx 中的 stx-map。它类似于 map,但它也适用于语法对象。

#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

要为用于转换语法的宏定义辅助函数,您需要将其放在 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)))

这个答案定义了两个版本:一个只支持命名参数,一个支持命名参数和位置参数。但是,它们都将使用 identifier->keyword 辅助函数。

刚刚命名的参数

define 的新版本采用 arg-name 并使用 identifier->keyword 辅助函数将它们转换为关键字,但由于它需要转换它们的语法列表,它使用 stx-map

然后它将关键字与 [arg-name default-val] 对组合在一起以创建 arg-kw [arg-name default-val] 的序列。使用具体代码,这会将 #:hi[hi "hello"] 分组以创建 #:hi [hi "hello"] 的序列,这是旧定义形式所期望的。

(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 ...))

这定义了一个 #%app 宏,它将被隐式插入到所有函数应用程序中。 (f stuff ...) 将扩展为 (#%app f stuff ...),因此 (greet4 [hi "hey"]) 将扩展为 (#%app greet4 [hi "hey"])

此宏将 (#%app greet4 [hi "hey"]) 转换为 (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 ... ...))

使用新的 define 形式:

> (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))

这些隐含地使用上面定义的新 #%app 宏:

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

省略参数使其使用默认值:

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

greet4这样的函数仍然可以在高阶函数中使用:

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

命名参数和位置参数

上述宏仅支持命名参数,因此不能定义使用位置参数的函数。但是,可以在同一个宏中同时支持位置参数和命名参数。

为此,我们必须制作方括号 [] "special" 以便 define#%app 可以区分命名参数和表达式。为此,我们可以使用 (syntax-property stx 'paren-shape),如果 stx 是用方括号写的,它将 return 字符 #\[

因此,要在 define 中指定位置参数,您只需使用普通标识符,而要使用命名参数,您将使用方括号。因此,参数规范可以是这些变体之一。你可以用 syntax class 来表达。

因为它被宏用来转换语法,所以它需要在 begin-for-syntaxidentifier->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])]))

然后可以这样定义define,使用arg:arg-spec指定arg使用arg-spec语法class.

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

对于给定的 argarg.norm ... 要么是一个事物的序列(对于位置参数),要么是两个事物的序列(对于命名参数)。那么由于 arg 本身可以出现任意次数,所以 arg.norm ... 在另一个省略号下,所以 arg.norm 在两个省略号下。

#%app宏将使用类似class的语法,但会稍微复杂一些,因为arg可以是任意表达式,它需要确保普通表达式不使用方括号。

同样,一个论点有两个变体。第一个变体需要是 使用方括号的表达式,第二个变体需要是用方括号包裹的名称和表达式。

(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)]))

#%app宏本身需要确保不与方括号一起使用。它可以用 #:fail-when 子句来做到这一点:

(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 ... ...))

现在 greet4 可以使用命名参数定义,但它也可以使用带有位置参数的 string-append

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

就像以前一样,省略参数会导致它使用默认值。

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

现在不同的是位置参数有效,

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

而且方括号[]不能再用来表达了

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

就像以前一样,greet4 可以用在高阶函数中,例如 compose

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

修改它以支持非函数定义

上面的 define 宏专门用于函数定义,以保持简单。但是,您也可以通过使用 define-syntax-parser 并指定多个案例来支持非函数定义。

这里的define-simple-macro定义

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

相当于在一个子句中使用define-syntax-parser

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

所以要支持多个子句,可以这样写:

(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 ...)])

那么这也将支持像(define x 0)这样的定义。