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-macro
和 identifier->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-syntax
和 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])]))
然后可以这样定义define
,使用arg:arg-spec
指定arg
使用arg-spec
语法class.
(define-simple-macro (define (fn arg:arg-spec ...) body ...+)
(old-define (fn arg.norm ... ...) body ...))
对于给定的 arg
,arg.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)
这样的定义。
可以在 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-macro
和 identifier->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-syntax
和 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])]))
然后可以这样定义define
,使用arg:arg-spec
指定arg
使用arg-spec
语法class.
(define-simple-macro (define (fn arg:arg-spec ...) body ...+)
(old-define (fn arg.norm ... ...) body ...))
对于给定的 arg
,arg.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)
这样的定义。