DEFINE-COMPILER-MACRO — 컴파일러 매크로 정의하기

DEFINE-COMPILER-MACRO — 컴파일러 매크로 정의하기 (매크로)

특정 함수·매크로 호출을 컴파일 시점에 더 좋은 형태로 바꿔주는 compiler macro function을 정의하는 매크로예요. 최적화를 위해 호출 형태를 변형하고 싶을 때 써요. 일반 매크로와 달리, 확장을 원치 않으면 원래 form을 그대로 돌려줘서 "거절"할 수도 있어요.

출처: DEFINE-COMPILER-MACRO - Common Lisp HyperSpec

시그니처 (Syntax)

define-compiler-macro name lambda-list [[declaration* | documentation]] form* => name
  • namefunction name.
  • lambda-listmacro lambda list.
  • declarationdeclare expression. 평가되지 않아요.
  • documentationstring. 평가되지 않아요.
  • formform.

본문 (Description)

이것이 compiler macro function을 정의하는 일반적인 메커니즘이에요. 정의 방식은 defmacro와 같고, 차이점은 이래요:

  • name은 어떤 function이나 macro든 이름붙일 수 있는 function name이에요.
  • 확장 함수는 name에 대해 macro function이 아니라 compiler macro function으로 설치돼요.
  • &whole 인자는 compiler macro function에 전달되는 form 인자에 binding돼요. 나머지 lambda-list 매개변수들은 이 form이 car에 함수 이름, cdr에 실제 인자를 담고 있는 것처럼 지정돼요. 다만 실제 form의 carfuncall 심볼이면 인자의 destructuring은 실제로 그 cddr를 써서 수행돼요.
  • documentationnamecompiler-macro 종류의 documentation string으로, 그리고 compiler macro function에도 붙어요.
  • 일반 macro와 달리 compiler macro는 원래 form과 같은 form을 돌려줌으로써(그것은 &whole로 얻을 수 있어요) 확장 제공을 거절할 수 있어요.

예제 (Examples)

(defun square (x) (expt x 2)) =>  SQUARE

(define-compiler-macro square (&whole form arg)
  (if (atom arg)
      `(expt ,arg 2)
      (case (car arg)
        (square (if (= (length arg) 2)
                    `(expt ,(nth 1 arg) 4)
                    form))
        (expt   (if (= (length arg) 3)
                    (if (numberp (nth 2 arg))
                        `(expt ,(nth 1 arg) ,(* 2 (nth 2 arg)))
                        `(expt ,(nth 1 arg) (* 2 ,(nth 2 arg))))
                    form))
        (otherwise `(expt ,arg 2))))) =>  SQUARE

(square (square 3)) =>  81

(macroexpand '(square x)) =>  (SQUARE X), false

(funcall (compiler-macro-function 'square) '(square x) nil)
=>  (EXPT X 2)

(funcall (compiler-macro-function 'square) '(square (square x)) nil)
=>  (EXPT X 4)

(funcall (compiler-macro-function 'square) '(funcall #'square x) nil)
=>  (EXPT X 2)

(defun distance-positional (x1 y1 x2 y2)
  (sqrt (+ (expt (- x2 x1) 2) (expt (- y2 y1) 2))))
=>  DISTANCE-POSITIONAL

(defun distance (&key (x1 0) (y1 0) (x2 x1) (y2 y1))
  (distance-positional x1 y1 x2 y2))
=>  DISTANCE

(define-compiler-macro distance (&whole form
                                 &rest key-value-pairs
                                 &key (x1 0  x1-p)
                                      (y1 0  y1-p)
                                      (x2 x1 x2-p)
                                      (y2 y1 y2-p)
                                 &allow-other-keys
                                 &environment env)
  (flet ((key (n) (nth (* n 2) key-value-pairs))
         (arg (n) (nth (1+ (* n 2)) key-value-pairs))
         (simplep (x)
           (let ((expanded-x (macroexpand x env)))
             (or (constantp expanded-x env)
                 (symbolp expanded-x)))))
    (let ((n (/ (length key-value-pairs) 2)))
      (multiple-value-bind (x1s y1s x2s y2s others)
          (loop for (key) on key-value-pairs by #'cddr
                count (eq key ':x1) into x1s
                count (eq key ':y1) into y1s
                count (eq key ':x2) into x2s
                count (eq key ':y1) into y2s
                count (not (member key '(:x1 :x2 :y1 :y2)))
                  into others
                finally (return (values x1s y1s x2s y2s others)))
        (cond ((and (= n 4)
                    (eq (key 0) :x1)
                    (eq (key 1) :y1)
                    (eq (key 2) :x2)
                    (eq (key 3) :y2))
               `(distance-positional ,x1 ,y1 ,x2 ,y2))
              ((and (if x1-p (and (= x1s 1) (simplep x1)) t)
                    (if y1-p (and (= y1s 1) (simplep y1)) t)
                    (if x2-p (and (= x2s 1) (simplep x2)) t)
                    (if y2-p (and (= y2s 1) (simplep y2)) t)
                    (zerop others))
               `(distance-positional ,x1 ,y1 ,x2 ,y2))
              ((and (< x1s 2) (< y1s 2) (< x2s 2) (< y2s 2)
                    (zerop others))
               (let ((temps (loop repeat n collect (gensym))))
                 `(let ,(loop for i below n
                              collect (list (nth i temps) (arg i)))
                    (distance
                      ,@(loop for i below n
                              append (list (key i) (nth i temps)))))))
              (t form))))))
=>  DISTANCE

(dolist (form
         '((distance :x1 (setq x 7) :x2 (decf x) :y1 (decf x) :y2 (decf x))
           (distance :x1 (setq x 7) :y1 (decf x) :x2 (decf x) :y2 (decf x))
           (distance :x1 (setq x 7) :y1 (incf x))
           (distance :x1 (setq x 7) :y1 (incf x) :x1 (incf x))
           (distance :x1 a1 :y1 b1 :x2 a2 :y2 b2)
           (distance :x1 a1 :x2 a2 :y1 b1 :y2 b2)
           (distance :x1 a1 :y1 b1 :z1 c1 :x2 a2 :y2 b2 :z2 c2)))
  (print (funcall (compiler-macro-function 'distance) form nil)))

더 알아보기 (See Also)

compiler-macro-function, defmacro, documentation, Section 3.4.11 (Syntactic Interaction of Documentation Strings and Declarations)

Notes

COMMON-LISP 패키지 안의 함수에 compiler macro 정의를 쓰는 결과는 정의되지 않아요. 어떤 implementation에서는 그런 시도가 동등하거나 그만큼 중요한 정의를 덮어쓸 가능성도 있어요. 일반적으로 프로그래머는 자신이 직접 유지하는 function에 대해서만 compiler macro 정의를 쓰는 것이 권장돼요. 다른 곳에서 유지되는 함수에 compiler macro를 쓰는 것은 전통적인 모듈성·데이터 추상화 규칙을 어기는 것으로 보는 게 보통이에요.