Programming

(Racket) 매크로 정복하기

steloflute 2023. 10. 30. 00:53

Racket 매크로는 hygienic macro인데, 쓰기 더 어려운 면이 있다. s-expression을 데이터로 바로 조작하기가 어렵다.

syntax->datum과 datum->syntax를 하여 번거롭게 해야 한다. (` 대신 #`를 사용하면 datum->syntax를 안 해도 되기는 하다.)

 

그러던 와중, Lisp: Common Lisp, Racket, Clojure, Emacs Lisp - Hyperpolyglot 에서 Racket의 define-syntax-rule 예제를 보게 되었다. Lisp의 defmacro와 비슷하게 구현하였는데, 다음과 같다:

 

(define-syntax-rule (rpn3 arg1 arg2 op)
  (eval ‘(,op ,arg1 ,arg2)))

위 코드는 REPL에서는 되는데 프로그램에서는 안 된다. (eval의 두번째 인자에 (make-base-namespace)를 넣어 줘야 한다.) 사실 다음과 같이 해야 작동한다:

(define-syntax-rule (rpn3 arg1 arg2 op)
  (eval #`(#,op #,arg1 #,arg2)))
(rpn3 2 3 +) ; (+ 2 3)

꽤 defmacro 비슷한 구현이다. 아니 이 예제는 왜 Racket documentation에 없는 것인가! 하지만 eval이 있기 때문에 느릴 수도 있다.

 

그래서 전에 매뉴얼대로 구현해본 것 (stx를 불편하게 손으로 destructure하여야 한다.)과 이번 것을 합하여 이렇게 하니 잘 되었다. 주의할 점은 인자도 textually 포함되기에 ' (quote)가 필요하다는 것이다.

 

#lang racket
(require compatibility/defmacro)
(define-syntax (evens stx)
  (let ([args (cdr (syntax->datum stx))])
    #`(list #,@(for/list ([i (in-naturals)]
                          [x (in-list args)]
                          #:when (even? i))
                 x))))

(define-syntax (evensb stx)
  (let ([args (cdr (syntax->datum stx))])
    (datum->syntax #f #`(quote #,(for/list ([i (in-naturals)]
                                            [x (in-list args)]
                                            #:when (even? i))
                                   x)))))

(define-syntax-rule (rpn arg1 arg2 op)
  (eval #`(#,op #,arg1 #,arg2)))

(define-syntax-rule (rpn2 arg1 arg2 op)
  (eval `(,op ,arg1 ,arg2) (make-base-namespace)))

(define-syntax-rule (evens2 . args)
  (eval #`(list #,@(for/list ([i (in-naturals)]
                              [x (in-list 'args)]
                              #:when (even? i))
                     x))))

(defmacro evens3 args
  `(list ,@(for/list ([i (in-naturals)]
                      [x (in-list args)]
                      #:when (even? i))
             x)))

(defmacro evens3b args
  `(quote ,(for/list ([i (in-naturals)]
                      [x (in-list args)]
                      #:when (even? i))
             x)))

(define-syntax-rule (evens4 args ...)
  (eval #`(list #,@(for/list ([i (in-naturals)]
                              [x (in-list '(args ...))]
                              #:when (even? i))
                     x))))

(define-syntax-rule (evens5 args ...)
  (for/list ([i (in-naturals)]
             [x (in-list '(args ...))]
             #:when (even? i))
    x))

;test
(displayln (evens 1 2 3 4 (+ 5 6))) ; '(1 3 11)
(displayln (evensb 1 2 3 4 (+ 5 6))) ; '(1 3 (+ 5 6))
(displayln (evens2 1 2 3 4 (+ 5 6))) ; '(1 3 11)
(displayln (evens3 1 2 3 4 (+ 5 6))) ; '(1 3 11)
(displayln (evens3b 1 2 3 4 (+ 5 6))) ; '(1 3 (+ 5 6))
(displayln (evens4 1 2 3 4 (+ 5 6))) ; '(1 3 11)
(displayln (evens5 1 2 3 4 (+ 5 6))) ; '(1 3 (+ 5 6))
(displayln (rpn 2 3 +)) ; 5
(displayln (rpn2 2 3 +)) ; 5

그래도 결국 defmacro가 가장 쉽다.

 

Clojure 방식이 최고인 것 같다. defmacro이면서 gensym을 편리하게 한다 symbol#을 통해서.

Clojure 1.11.0
user=> (defmacro two-list [x] `(let [arg# ~x] (list arg# arg#)))
#'user/two-list
user=> (two-list 1)
(1 1)