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)