Как заставить макросы работать с `match`? - PullRequest
2 голосов
/ 30 марта 2020

Я пытаюсь написать что-то вроде конечного автомата для калькулятора, который я сделал, используя racket/gui, и я решил использовать сочетание case и match для его реализации. Для конкретного состояния и символа я выполню произвольный код и верну следующее состояние машины. Простой пример:

  (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. Я хотел бы, чтобы ответы предоставили мне способ получить желаемую форму, и был бы признателен за объяснение того, почему мои предыдущие попытки не сработали.

1 Ответ

4 голосов
/ 30 марта 2020

Давайте сначала поговорим о том, почему ваш :NOT-0: не работает. Прежде всего, макрос является синтаксическим преобразователем объектов. То есть функция от объекта синтаксиса к объекту синтаксиса. Поэтому вам нужно написать:

(define-syntax :NOT-0:
  (lambda (stx) #'(? is-non-zero-digit?)))

или использовать сокращенную форму:

(define-syntax (:NOT-0: stx)
  #'(? is-non-zero-digit?))

Но исправленный код тоже не совсем работает. Причина в том, что макросы Racket по умолчанию развернуты "снаружи внутрь". Это означает:

(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, предоставляют «макросы определения макросов», которые можно использовать для определения bar таким образом, что foo понимает, что bar должен быть расширен в первую очередь. Для технических деталей см. Макросы, которые работают вместе , автор Matthew Flatt и др.

. Для вашей конкретной проблемы match Racket предоставляет 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 будет рассматривать его как идентификатор для привязки совпадающего значения к.


Лично я не считаю, что match Ракетта здесь уместен. Обычно, когда есть много предложений (? predicate), предлагается вместо этого преобразовать их в cond:

(cond
  [(predicate-1? symbol) ...]
  [(predicate-2? symbol) ...]
  ...)

Наконец, вы можете создать свой собственный match, если вы действительно этого хотите. быть в той форме, которую вы хотите. И вы можете расширить свой match до cond или Racket match, как вы хотите. В качестве бонуса вы будете иметь полный контроль над подчиненными формами, что позволит вам поменять местами «действие» и «состояние». Вот небольшой пример.

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