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)

 

 

 

 

'Programming' 카테고리의 다른 글

(Racket) 그래프 그리기 (plot)  (0) 2023.12.19
Code Complete, Second Edition eBook PDF  (0) 2023.11.11
(Common Lisp) set, setq 비교  (0) 2023.10.26
VS Code에서 Common Lisp 프로그래밍  (0) 2023.10.26
newLISP 10.7.1 installer  (0) 2023.10.19