Blog

Common Lisp, 매크로란 무엇인가

March 13, 2014

Common Lisp, 매크로란 무엇인가

What is Macro?

Lisp 에서 매크로가 무엇인지 잠깐이나마 느낀 순간을 공유해 보려고 한다.

1. Macro-Expansion Time

Lisp 에는 Macro 가 코드로 치환되는 macro-expansion time 과 실제 프로그램이 돌아가는 runtime 이 따로 있다. macro-expansion time 은 흔히 compile time 이라 말하기도 하는데, 다음 예제를 통해 macro-expansion time 존재를 알아볼 수 있다.

(defmacro five-macro (arg)
    `(+ 5 ,arg))

(macroexpand-1 '(five-macro 2)
> (+ 5 2)

(five-macro 2)
> 7

macroexpand-1 명령어를 이용해 macro-expansion timedefmacro 를 통해 정의된 매크로가 어떻게 확장되는지 알 수 있다. 그리고 이렇게 확장된 (+ 5 2) 라는 코드!는 런타임에 실행(평가, evaluated) 되어 7 이라는 결과값을 돌려주게 된다.

여기서 한가지 질문을 할 수 있다. defun 이랑 뭐가 다른데? 해답을 알고 싶은가?

(defun five-function ()
    '(+ 5 2))

(five-function 2)
> (+ 5 2)

(eval (five-function 2))
> 7

defun 으로 정의된 함수는 당연히 macro-expansion time 에 확장되지 않으므로, 실행시점에 ‘(+ 5 2) 로서 평가된다.

2. Generateing Code

위에서 우리는 매크로가 실행 시점(Runtime)이 아닌 매크로 확장 타임(Macro-Expansion Time) 에 코드로 치환된다는걸 알아냈다. 이게 무슨 의미일까?

코드를 조작할 수 있다는 의미다.

Runtime 전까지, 모든 것들은 평가되지 않는다. 따라서 defmacro 가 받는 모든 인자들은 코드 그 자체 다. 무슨말인고 하니, 다음 예제를 보자.

(defmacro print-macro (arg)
    `(print ,arg))

(macroexpand-1 '(print-macro (+ 2 3)))
> (PRINT (+ 2 3))

따라서 defmacro 를 이용해 인자를 가지고 어떤 작업을 해도, 그 작업은 macro-expansion time 에 일어나므로, 실행시간 이전이어서, 그 인자는 ‘코드 그 자체’ 라는 것이다. 위에 코드에서도 arg(+ 2 3) 이므로, 코드 그 자체임을 알 수 있다.

이와 달리 defun 을 이용해 함수를 정의하면 일반적인 리습 평가 양식이 적용되므로, 모든 인자가 평가되어 함수로 전달된다. 다음을 보자.

(defun print-function (arg)
    `(print ,arg))

(print-function (+ 2 3))
> (PRINT 5)

(+ 2 3) 이라는 인자가 런타임에 평가되어 5 로써, print-function 에 전달되었다.

결국 Lisp 에서 매크로를 이용해 우리가 할 수 있는 일은, macro-expansion time 에 코드를 조작하거나, 변경할 수 있다는 이야기다. 왜냐하면 defmacro 를 이용해 선언한 매크로가 확장되는 시점에는 모든 인자들이 아직 평가되지 않았으므로 (+ 2 3)5 가 아니라 (+ 2 3) 이라는 그 자체로서 존재한다.

3. Practical Example

Practical Common Lisp 챕터 3장에 보면, Lisp 을 이용해 데이터베이스를 만드는 예제가 나온다.

그 중간에, 다음과 같은 코드들이 있는데 데이터베이스에 저장된 데이터를 DBMS 처럼 select 하는 예제다.

(defparameter *person-db* '((:name "Anster" :age 26 :address "Seoul")))

(defun select (selector-fn)
  (remove-if-not selector-fn *person-db*))

(defun where (&key name age address)
  #'(lambda (row)
      (and
       (if name    (equal (getf row :name)  name)  t)
       (if age   (equal (getf row :age) artist) t)
       (if address   (equal (getf row :address) address) t))))

이렇게 코드 정의를 하고, 아래와 같은 결과를 얻을 수 있다.

$ (select (where :name "Anster"))
> ((:NAME "Anster" :AGE 26 :ADDRESS "Seoul"))

그런데, 두가지 문제점이 있다. 데이터베이스에 필드가 점점 늘어나면, 이를테면 phone 이라던가, 그럴때마다 where 함수를 수정해야 하는 일들이 발생한다.

또한 if 를 통해 :name 과 같은 키워드가 입력 되었는지, 안되었는지를 런타임에 검사하는것도 문제가 있다.

그래서 매크로를 이용해 입력받은 인자를 이용해서 위의 코드를 변경해 보았다. 매크로를 이용하면, 런타임이 아닌 macro-expansion time 에 코드를 조작해서 인자가 몇개가 오든지 그걸 모두 검사 해 내는 코드를 생성해 낼 수 있기 때문이다.

(defmacro where (&rest args)
  `#'(lambda (person)
       (and ,@(loop while args
            collect `(equal (getf person ,(pop args)) ,(pop args))))))

References

  1. http://simonyim.tistory.com/entry/3PracticalASimpleDataBaseRev2
  2. http://c2.com/cgi/wiki?LispMacro
  3. The Common Lisp Cookbook – Macro and Backquote
  4. The Power of Lisp MACROS
  5. http://www.chemie.fu-berlin.de/chemnet/use/info/elisp/elisp_13.html
  6. Practical Common Lisp Chapter 7
Array