如何制作与“匹配”一起使用的宏?
How to make macros that work with `match`?
我正在尝试为我使用 racket/gui
制作的计算器编写类似有限状态机的东西,我决定混合使用 case
和 match
来实施它。对于特定的状态和符号,我将执行一些任意代码和 return 机器的下一个状态。一个简单的例子:
(case current-state
[(state-1)
(match symbol
[(? predicate-1?)
(some-action)
next-state]
[(? predicate-2?)
(some-action)
next-state]
; ...
)]
; ...
)
不过,我想让它更易于阅读,并且想玩玩宏。我会经常使用的一些谓词,并希望以更短的方式编写它们。而且我不喜欢下一个状态在一系列动作结束时丢失。我想要这些信息的前沿和中心。所以我更愿意写这样的东西:
(case current-state
[(state-1)
(match symbol
[:PRED-1: next-state
(some-action)]
[:PRED-2: next-state
(some-action)]
; ...
)]
; ...
)
我对宏没有太多经验,而且我早期的尝试都出错了。我的第一次部分尝试只是谓词宏。这是一个简单的例子:
(define (in-list value lst)
(if (list? (member value lst))
#true
#false))
(define (is-non-zero-digit? symbol)
(in-list symbol '(1 2 3 4 5 6 7 8 9)))
(define-syntax :NOT-0:
#'(? is-non-zero-digit?))
(match 0
[:NOT-0: 'wrong]
[_ 'right])
; 'wrong
我不确定为什么会这样。我认为 :NOT-0:
会扩展到 (? is-non-zero-digit?)
。我尝试的另一件事是通过定义一个名为 transition
:
的宏来获得我想要的顺序
; defined earlier in file
(define-syntax-rule
(transition pattern next-state action ...)
[pattern action ... next-state])
; ...
; the below is from a rackunit test
(define a-variable 0)
(define (side-effect)
(set! a-variable 1))
(define result
(match 0
(transition (? is-non-zero-digit?) 'wrong (side-effect))
[_ 'right]))
(check-equal? result 'right)
(check-equal? a-variable 1))
但是我收到错误 state-machine.rkt:220:21: ?: unbound identifier
。我希望获得答案,以便为我提供获得所需形式的方法,并且希望能解释为什么我之前的尝试没有成功。
先说说为什么你的:NOT-0:
不行。首先,宏是语法对象转换器。也就是说,一个从句法对象到句法对象的函数。所以你需要写:
(define-syntax :NOT-0:
(lambda (stx) #'(? is-non-zero-digit?)))
或使用其 shorthand 形式:
(define-syntax (:NOT-0: stx)
#'(? is-non-zero-digit?))
但是更正后的代码也不能正常工作。原因是默认情况下 Racket 宏被扩展 "outside in"。这意味着:
(define-syntax-rule (foo (#:foo x))
x)
(define-syntax-rule (bar x)
(#:foo x))
(foo (bar 1)) ; doesn't work, because `foo` is expanded first, and it couldn't find #:foo
大多数想让用户扩展其功能的宏,如 foo
将提供一个 "macro defining macros",您可以使用它以 foo
的方式定义 bar
明白应该先展开bar
。有关技术细节,请参阅 Matthew Flatt 等人的Macros that Work Together
对于您的特定问题,Racket 的 match
提供了 define-match-expander
,这是一个定义宏的宏,我在上面描述过。你可以这样使用它:
(define-match-expander :NOT-0:
;; can also use syntax-case on stx to further ensure that stx must have a particular shape.
(lambda (stx) #'(? is-non-zero-digit?)))
(define (is-non-zero-digit? symbol)
;; no need to define in-list. member alone would suffice
(member symbol '(1 2 3 4 5 6 7 8 9)))
(match 0
[(:NOT-0:) 'wrong]
[_ 'right])
请注意,您需要在 :NOT-0:
周围加上括号。如果你有一个裸 :NOT-0:
,match
会将其视为将匹配值绑定到的标识符。
就我个人而言,我认为 Racket 的 match
在这里不合适。通常,当 (? predicate)
子句很多时,建议您将其转换为 cond
而不是:
(cond
[(predicate-1? symbol) ...]
[(predicate-2? symbol) ...]
...)
最后,您可以创建自己的 match
如果您真的希望它成为您想要的形式。您可以根据需要将 match
扩展为 cond
或 Racket 的 match
。作为奖励,您将完全控制其中的子表单,允许您交换 "action" 和 "state"。这是一个小例子。
(define-syntax-rule (match e [pred e*] ... [#:else e-else])
(let ([v e]) ; so that we evaluate e only once
(cond [(pred v) e*] ... [else e-else])))
(match 0
[is-non-zero-digit? 'wrong]
[#:else 'right])
(require (only-in racket/match [match r:match]))
;; Racket's match is still available via r:match
听从@sorawee的建议,我制作了自己的宏,如果有人对我的最终结果感兴趣:
(define-syntax-rule
(transition to-evaluate
[pred next-symbol body ... ] ...
[#:else else-next-symbol else-body ...])
(let ([v to-evaluate])
(cond
[(pred v) body ... next-symbol] ...
[else else-body ... else-next-symbol])))
它将转换如下表达式:
(transition symbol
[is-digit? '1st-operand
(send accumulator-register push symbol)]
[is-operator? 'got-op
(set! current-operator symbol)]
[#:else (clear-calculator)])
至
(cond
[(is-digit? symbol)
(send accumulator-register push symbol)
'1st-operand]
[(is-operator? symbol)
(set! current-operator symbol)
'got-op]
[else (clear-calculator)])
我正在尝试为我使用 racket/gui
制作的计算器编写类似有限状态机的东西,我决定混合使用 case
和 match
来实施它。对于特定的状态和符号,我将执行一些任意代码和 return 机器的下一个状态。一个简单的例子:
(case current-state
[(state-1)
(match symbol
[(? predicate-1?)
(some-action)
next-state]
[(? predicate-2?)
(some-action)
next-state]
; ...
)]
; ...
)
不过,我想让它更易于阅读,并且想玩玩宏。我会经常使用的一些谓词,并希望以更短的方式编写它们。而且我不喜欢下一个状态在一系列动作结束时丢失。我想要这些信息的前沿和中心。所以我更愿意写这样的东西:
(case current-state
[(state-1)
(match symbol
[:PRED-1: next-state
(some-action)]
[:PRED-2: next-state
(some-action)]
; ...
)]
; ...
)
我对宏没有太多经验,而且我早期的尝试都出错了。我的第一次部分尝试只是谓词宏。这是一个简单的例子:
(define (in-list value lst)
(if (list? (member value lst))
#true
#false))
(define (is-non-zero-digit? symbol)
(in-list symbol '(1 2 3 4 5 6 7 8 9)))
(define-syntax :NOT-0:
#'(? is-non-zero-digit?))
(match 0
[:NOT-0: 'wrong]
[_ 'right])
; 'wrong
我不确定为什么会这样。我认为 :NOT-0:
会扩展到 (? is-non-zero-digit?)
。我尝试的另一件事是通过定义一个名为 transition
:
; defined earlier in file
(define-syntax-rule
(transition pattern next-state action ...)
[pattern action ... next-state])
; ...
; the below is from a rackunit test
(define a-variable 0)
(define (side-effect)
(set! a-variable 1))
(define result
(match 0
(transition (? is-non-zero-digit?) 'wrong (side-effect))
[_ 'right]))
(check-equal? result 'right)
(check-equal? a-variable 1))
但是我收到错误 state-machine.rkt:220:21: ?: unbound identifier
。我希望获得答案,以便为我提供获得所需形式的方法,并且希望能解释为什么我之前的尝试没有成功。
先说说为什么你的:NOT-0:
不行。首先,宏是语法对象转换器。也就是说,一个从句法对象到句法对象的函数。所以你需要写:
(define-syntax :NOT-0:
(lambda (stx) #'(? is-non-zero-digit?)))
或使用其 shorthand 形式:
(define-syntax (:NOT-0: stx)
#'(? is-non-zero-digit?))
但是更正后的代码也不能正常工作。原因是默认情况下 Racket 宏被扩展 "outside in"。这意味着:
(define-syntax-rule (foo (#:foo x))
x)
(define-syntax-rule (bar x)
(#:foo x))
(foo (bar 1)) ; doesn't work, because `foo` is expanded first, and it couldn't find #:foo
大多数想让用户扩展其功能的宏,如 foo
将提供一个 "macro defining macros",您可以使用它以 foo
的方式定义 bar
明白应该先展开bar
。有关技术细节,请参阅 Matthew Flatt 等人的Macros that Work Together
对于您的特定问题,Racket 的 match
提供了 define-match-expander
,这是一个定义宏的宏,我在上面描述过。你可以这样使用它:
(define-match-expander :NOT-0:
;; can also use syntax-case on stx to further ensure that stx must have a particular shape.
(lambda (stx) #'(? is-non-zero-digit?)))
(define (is-non-zero-digit? symbol)
;; no need to define in-list. member alone would suffice
(member symbol '(1 2 3 4 5 6 7 8 9)))
(match 0
[(:NOT-0:) 'wrong]
[_ 'right])
请注意,您需要在 :NOT-0:
周围加上括号。如果你有一个裸 :NOT-0:
,match
会将其视为将匹配值绑定到的标识符。
就我个人而言,我认为 Racket 的 match
在这里不合适。通常,当 (? predicate)
子句很多时,建议您将其转换为 cond
而不是:
(cond
[(predicate-1? symbol) ...]
[(predicate-2? symbol) ...]
...)
最后,您可以创建自己的 match
如果您真的希望它成为您想要的形式。您可以根据需要将 match
扩展为 cond
或 Racket 的 match
。作为奖励,您将完全控制其中的子表单,允许您交换 "action" 和 "state"。这是一个小例子。
(define-syntax-rule (match e [pred e*] ... [#:else e-else])
(let ([v e]) ; so that we evaluate e only once
(cond [(pred v) e*] ... [else e-else])))
(match 0
[is-non-zero-digit? 'wrong]
[#:else 'right])
(require (only-in racket/match [match r:match]))
;; Racket's match is still available via r:match
听从@sorawee的建议,我制作了自己的宏,如果有人对我的最终结果感兴趣:
(define-syntax-rule
(transition to-evaluate
[pred next-symbol body ... ] ...
[#:else else-next-symbol else-body ...])
(let ([v to-evaluate])
(cond
[(pred v) body ... next-symbol] ...
[else else-body ... else-next-symbol])))
它将转换如下表达式:
(transition symbol
[is-digit? '1st-operand
(send accumulator-register push symbol)]
[is-operator? 'got-op
(set! current-operator symbol)]
[#:else (clear-calculator)])
至
(cond
[(is-digit? symbol)
(send accumulator-register push symbol)
'1st-operand]
[(is-operator? symbol)
(set! current-operator symbol)
'got-op]
[else (clear-calculator)])