User Tools

Site Tools


study:anaphoric_macros

This is an old revision of the document!


Chapter 6. Anaphoric Macros

6.1 More Phors?

anaphoric 매크로

  • anaphor.(sg), anaphora.(pl) : 2가지 의미
    • 강조를 위해 문장의 앞 부분에서 같은 단어를 반복하는 것 : “She didn't speak. She didn't stand. She didn't even look up when we came in.”
    • 앞에서 사용된 단어에 대한 참조(전방참조, 대명사) : “Sally arrived, but nobody saw her”
  • 폴 그래함의 On Lisp에서 처음 사용됨.
    • alambda(anaphoric lambda), aif(anaphoric if).
    • anaphor의 첫 알파벳 a를 따라 이름 지었다.
  • 매크로에 입력된 문구의 자유변수를 계획적으로 포획하는 매크로이다.
  • transparent specification : 투명한(보이지 않는, 은밀한) 설정.
    • 매크로 확장을 넘어서는 제어가 가능.
    • 조합을 통한 매크로 확장.

alambda

alambda는 가장 대표적인 anaphoric 매크로이다.

;; Graham's alambda
(defmacro alambda (parms &body body)
  `(labels ((self ,parms ,@body))
     #'self))

label은 common lisp에서 지역함수를 정의하기 위해 사용된다.

alambda 매크로는 자신의 파라미터 parms와 body를 파라미터로 하는 self라는 지역 함수를 정의한 후, 그 지역함수를 람다 함수로 리턴하는 문구를 만든다.

#'foo = (function foo) 이다.

common lisp에서 심볼(foo)는 함수(function)와 값(value)를 동시에 지정할 수 있기 때문에, 그냥 foo 하면 value로 인식하게 되고, (function foo)해야 함수로 인식된다. function 은 common lisp의 special form이다.

위 매크로 정의에서 #'self가 아닌 self로 하면 'undefined variable'이라는 에러가 발생하게 된다.

(alambda (n)
  (if (> n 0)
    (cons
      n
      (self (- n 1)))))

alambda는 위 코드처럼 사용되는데, self는 바로 자기 자신을 재귀하는 것으로 아주 자연스럽게 표현도고 있음을 볼 수 있다. 이 코드를 보면 람다 함수는 그 이름이 없기 때문에 그 자체로는 재귀를 할 수 없다는 일반적인 상식을 깨진다. alambda는 self 심볼을 투명하게(은밀하게) 바인딩하고 있기 때문에 (투명한 설정), 불의의 변수 포획은 결코 문제가 되지 않는다.

aif

On Lisp의 다른 anaphoric 매크로인 aif는 then절에서 사용하기 위해 조건절의 결과를 it에 바인딩한다.

;; Graham's aif
(defmacro aif (test then &optional else)
  `(let ((it ,test))
     (if it ,then ,else)))

generalised booleans(일반화된 참진값)

common lisp에서는 nil이 아닌 모든 값은 참이다. 반면 스킴에서는 참진을 표시하기 위한 값(false/true)을 명시적으로 따로 두고 있는데, 이로 인해 타입에 대한 제약이 가해진다. 하지만 스킴에도 사실은 if, cond, and,등이 참진값이 아닌 값을 받도록 하는 장치가 있다. (좀 이상하지 않나?) common lisp에서는 모든 것이 참진값이다. 올바르게 설계돈 것이다.

비위생적 매크로

aif나 alambda는 lexical transparency를 위배한다. 즉 비위생적 매크로라고 할 수 있다. 하지만 위생 시스템템은 어느 정도 실력이 갖추어지면 버려야 할 장난감이다.

lexical clojure : let over lambda

anaphoric 매크로가 read 매크로에 의해 사용될 수 있다.

6.2. Sharp-Backquote

read anaphora

anaphoric은 일반적인 매크로에서가 아니라 read 매크로에서도 사용될 수 있다.

(defun |#`-reader| (stream sub-char numarg)
  (declare (ignore sub-char))
  (unless numarg (setq numarg 1))
  `(lambda ,(loop for i from 1 to numarg
                  collect (symb 'a i))
     ,(funcall
        (get-macro-character #\`) stream nil)))
 
(set-dispatch-macro-character
  #\# #\` #'|#`-reader|)

set-dispatch-macro-character 함수는 lisp reader가 #\#('#' 문자)를 dispatch 문자로, #\`('`' 문자)를 sub-char로 읽었을 때 |#`-reader|라는 매크로 문자 함수를 호출되도록 reader를 설정한다. 결국 #'문자를 읽을 때마다 |#`-reader| 함수가 호출되는 것이다. lisp reader는 dispatch 문자('#')를 읽은 후 digit 문자(0~9)를 non-digit 문자를 만날 때까지 읽는다. lisp reader는 읽어들인 digit 문자들은 정수로 바꾸고(digit문자가 없으면 nil로), non-digit 문자와 함께 각각 |#`-reader| 매크로 문자 함수의 numarg와 sub-char 파라미터로 넘긴다. stream 파라미터는 lisp reader가 읽어들인 이후의 위치를 가리키는 stream이 된다.

이제 |#`-reader| 함수는 stream 파라미터로는 reader가 읽은 후의 스트림을 받고, sub-char는 `문자가, numarg에는 아규먼트 수가 온다. 이 함수에서 sub-char는 무시되고, numarg은 nil이면 1로 설정된다.

symb 함수는 1장에서 선언된 함수이다.

(defun mkstr (&rest args)
  (with-output-to-string (s)
    (dolist (a args) (princ a s))))
 
(defun symb (&rest args)
  (values (intern (apply #'mkstr args))))

이 |#`-reader| 함수는 결국 lambda 문구를 리턴하는 표현식을 만든다.

아래 코드는 이 lambda 함수의 파리미터 리스트 문구를 만들어 낸다.

(loop for i from 1 to numarg
                  collect (symb 'a i))

numarg까지 for 루프를 돌면서 a로 시작하는 심볼의 리스트를 만들어 내는 것이다.

아래는 lambda 함수의 body에 해당하는 문구를 만들어 내는 역할을 한다.

(funcall
        (get-macro-character #\`) stream nil)))

get-macro-character 함수는 #\`('`'문자)라는 매크로 문자에 설정된 매크로 문자 함수를 리턴한다. 결국 위 표현식은 |#`-reader| 함수가 읽은 이후의 문자들을 읽어(즉 #` 이후) '`' 매크로 문자에 해당하는 매크로 처리를 하는 것이다.

아래는 위 매크로를 정의한 이후 사용법을 보여준다.

 '#`((,a1))
 
(LAMBDA (A1)
  `((,A1)))
 (mapcar (lambda (a)
            (list a ''empty))
    '(var-a var-b var-c))
 
((VAR-A 'EMPTY)
 (VAR-B 'EMPTY)
 (VAR-C 'EMPTY))
 (mapcar (lambda (a)
            `(,a 'empty))
    '(var-a var-b var-c))
 
((VAR-A 'EMPTY)
 (VAR-B 'EMPTY)
 (VAR-C 'EMPTY))
 (mapcar #`(,a1 'empty)
    '(var-a var-b var-c))
 
((VAR-A 'EMPTY)
 (VAR-B 'EMPTY)
 (VAR-C 'EMPTY))
 '#2`(,a1 ,a2)
 
(LAMBDA (A1 A2)
  `(,A1 ,A2))
 (let ((vars '(var-a var-b var-c)))
    (mapcar #2`(,a1 ',a2)
      vars
      (loop for v in vars
            collect (gensym
                      (symbol-name v)))))
 
((VAR-A '#:VAR-A1731)
 (VAR-B '#:VAR-B1732)
 (VAR-C '#:VAR-C1733))
 (#3`(((,a1)) ,@a2 (,a3))
      (gensym)
      '(a b c)
      'hello)
 
(((#:G1734)) A B C (HELLO))
 (#3`(((,@a2)) ,a3 (,a1 ,a1))
      (gensym)
      '(a b c)
      'hello)
 
(((A B C)) HELLO (#:G1735 #:G1735))

6.3 Alet and finite state machine

let의 목적은 binding을 통해 let 문구(form)에서 사용될 변수를 미리 포획하는 것이다. let을 매크로로 확장하면 let 문구(form)에 주어진 모든 문구(form)들에 대한 완전한 통제를 갖게되는데, 특히 마지막 문구는 let 문구(form)에서 리턴되는 문구이기 때문에 중요하다. 그래서 이것을 this라는 심볼로 지정하고 포획할 수 있는데, 특히 let이 lambda를 리턴하게 되는 경우에 이것은 매우 유용하다.

(defmacro alet% (letargs &rest body)
  `(let ((this) ,@letargs)
     (setq this ,@(last body))
     ,@(butlast body)
     this))

Alet%은 lambda 문구(form)에서 중복하기 싫은 초기화 코드가 있을 때 사용하면 유용하다. this는 리턴되는 lambda 문구(form)에 바인딩되기 때문에, 매크로 내에서 실행 순서를 바꾸어서 리턴되기 전에 이 lambda 문구를 실행할 수 있다. 다음은 Alet%의 사용예이다.

> (alet% ((sum) (mul) (expt))
    (funcall this :reset)
    (dlambda
      (:reset ()
        (psetq sum 0
               mul 1
               expt 2))
      (t (n)
        (psetq sum (+ sum n)
               mul (* mul n)
               expt (expt expt n))
        (list sum mul expt))))
 
#<Interpreted Function>

dlambda는 5.7 절에서 소개된 dispatch lambda로, dlambda의 각 문구의 첫 요소를 case로 해서 분기하는 역할을 한다.

이 매크로가 어떻게 확장되는지 보자.

우선 파라미터 letargs와 body는 다음과 같이 할당된다.

   letargs <== ((sum) (mul) (expt))
   body <== ((funcall this :reset) (dlambda ...))

매크로의 let 문구의 바인딩 문구에서 ,@letargss는 괄호가 한 번 풀리고 평가된다.

   (let ((this) ,@letargs)) ==> (let ((this) (sum) (mul) (expt)))

여기서 this, sum,mul, expt는 nil로 초기화된다.

다음 매크로 문구에서

(setq this ,@(last body))

last는 그 결과가 리스트이기 때문에 한 번 괄호를 벗겨서 dlambda 문구를 평가한 후 this 심볼에 바인딩한다.

> (last '(1 2 3 4) 2)
=> (3 4)
> (last '(1 2 3 4) 1)
=> (4)
> (last '(1 2 3 4))
=> (4)

위에서 (last body)는 1)이다.

다음 매크로 문구는 body의 첫 문구, (funcall this :reset)를 평가한다.

,@(butlast body)

(butlast body)는 2)이다. 이것을 괄호를 벗기고 평가하는데, this는 이미 위에서 dlambda의 평가 결과 즉 람다 함수이인데, :reset으로 분기되어 평가된다. 결국 이것은 dlambda의 :reset 함수를 호출하여 초기화하는 코드를 수행하는 것이다.

이후 alet% 매크로는 this, 즉 dlambda를 리턴한다.

결국 alet% 매크로는 body의 문구 중 마지막 리턴되는 문구를 제외한 문구를 미리 한 번 수행한 후 마지막 리턴되는 문구를 this 심볼에 바인딩 한 후 리턴하는 것이다.

다음은 alet% 함수의 사용예이다.

> (loop for i from 1 to 5 collect (funcall * 2))
 
((2 2 4)
 (4 4 16)
 (6 8 256)
 (8 16 65536)
 (10 32 4294967296))

common lisp은 불변값을 사용하지 않기 때문에 지역 변수들이 상태를 유지하게 된다.

이제 :reset 메소드를 호출하게 되면 상태는 초기화된다.

> (funcall ** :reset)
 
NIL

새로운 값으로 시작할 수 있다.

> (loop for i from 1 to 5 collect (funcall *** 0.5))
 
((0.5 0.5 1.4142135)
 (1.0 0.25 1.1892071)
 (1.5 0.125 1.0905077)
 (2.0 0.0625 1.0442737)
 (2.5 0.03125 1.0218971))

alet%은 let body의 문구(form)의 평가 순서를 바꾼다. 따라서 let body 문구들은 실행순서에 무관해야 한다. 리턴되는 마지막 문구가 상수라면 이러한 실행순서 변경은 문제가 되지 않는다.

간접 참조(indirection)

위에서 Alet%는 마지막 문구를 리턴했다. 그 문구를 람다 함수였다.

하지만 리턴되는 것이 직접 어떤 일을 하는 함수가 아니라, let 스코프안의 함수들 중 하나를 골라 수행하는 함수가 될 수 있다. 이렇게 되면 컴파일 시간이 아니라 실행 시간에 실행될 함수를 변경할 수 있게 된다.

이러한 기법을 간접 참조라고 하는데, 이것은 프로그래밍 전반에 걸쳐 두루 사용되는 기법이다.

alet은 간접 참조를 사용한 alet% 버젼이다.

(defmacro alet (letargs &rest body)
  `(let ((this) ,@letargs)
     (setq this ,@(last body))
     ,@(butlast body)
     (lambda (&rest params)
       (apply this params))))

alet 매크로는 alet% 매크로와는 4번째 줄까지는 같다. 다른 것은 단지 리턴되는 마지막 문구이다.

물론 이 문구가 둘 다 람다 함수라는 점에서는 같기는 하지만, 그 람다 함수가 수행하는 역할이 다르다. alet%는 let 문구에 있는 마지막 람다 함수가 this로 바인딩되고 리턴되는 반면, alet은 마지막 람다 함수를 this로 바인딩한 후 이 this를 호출하는 또 다른 람다 함수를 리턴한다.

이렇게 되면 alet 매크로가 만든 closure를 호출할 때 실행되는 함수를 바꿀 수 있게 된다. 즉 alet over alambda 라는 패턴을 사용하여 상호 참조하는 함수 쌍을 만들어 교대로 함수 호출을 변경할 수 있다.

다음은 전형적인 카운터 클로져인데, 수를 인자로 받으면 주어진 방향에 따랏 증가하거나 감소하고, invert를 인자로 받으면 그 방향을 바꾼다.

> (alet ((acc 0))
    (alambda (n)
      (if (eq n 'invert)
        (setq this
              (lambda (n)
                (if (eq n 'invert)
                  (setq this #'self)
                  (decf acc n))))
        (incf acc n))))
 
#<Interpreted Function>

일단 alet over alambda 클로져가 어떻게 동작하는지 먼저 보자.

다음은 이 클로져를 alet-test 심볼에 바인딩한다.

> (setf (symbol-function 'alet-test) *)
 
#<Interpreted Function>

우선 먼저 10을 인자로 해서 호출하면 초기값 0에서 10 증가한 값이 리턴된다.

> (alet-test 10)
 
10

이제 'invert로 호출하면 방향이 바뀐다.

> (alet-test 'invert)
 
#<Interpreted Function>

이제 숫자 3으로 호출하면 이전 값 10에서 3이 감소한 7이 리턴된다.

> (alet-test 3)
 
7

다시 방향을 바꾼다.

> (alet-test 'invert)
 
#<Interpreted Function>

이번엔 숫자 5로 호출하면 7에서 5 증가한 12가 리턴된다.

> (alet-test 5)
 
12

자 이제 alet 매크로가 만든 카운터 클로져가 어떻게 저렇게 동작하는지 알아보자.

2개의 람다 함수

alambda

우선 살펴볼 것이 alet에서 alambda가 사용되고 있다는 것인데, 이것은 6.1 절에서 소개된 바 있다. alambda는 람다 함수 자체를 self로 지정할 수 있게 한다. 또한 alambda 문구는 최초에 alet에서 도입되는 심볼 this에도 바인딩된다.

카운터 클로져에서 alambda에 해당하는 람다 함수는 self 심볼에 바인딩되고, 두번째 setq 문구에서 사용되고 있다.

lambda

alet 문구에는 또 하나의 람다 함수가 있는 것을 볼 수 있다. 이 함수는 alambda 함수와 함께 방향이 전환될 때마다 서로 호출이 변경되는 함수이다.

2개의 setq

(setq this (lambda …))

이 문구는 alet에서 도입된 심볼 this를 lambda 함수로 바인딩한다.

(setq this #'self)

이 문구는 alet에서 도입된 심볼 this를 alambda 함수로 바인딩한다. self는 alambda에서 도입된 심볼로 함수를 가리키기 위해 #' 를 사용했다.

2개의 if 문구

(if (eq n 'invert) …)

2 개의 똑같은 if 문구가 사용되는데, 이 문구는 alambda 함수와 lambda 함수에서 인자 invert임을 검사한다.

alet으로 리턴되는 클로져의 람다 함수는 this 심볼에 자신이 받은 파라미터를 전달하여 호출한다는 점에 주목한다. 최초에 this 심볼은 alambda의 람다 함수이다.

  1. (alet-test 10) : 카운터 클로져 alet-test가 10을 인자로 호출되면 첫 번째 if 문구의 else절로 분기되어 (incf acc n) 문구가 수행되고, acc가 최초 0에서 10으로 증가한다.
  2. (alet-test 'invert) : 'invert가 인자로 들어오면 첫 번째 if 문구의 then 절로 분기되어 (setq this (lambda …)) 문구가 수행되고, this가 alambda에서 lambda 함수로 바뀐다.
  3. (alet-test 3) : 다시 숫자 3으로 this가 가리키는 람다 함수로 호출이 전달되어 두번째 if 문구의 else 절로 분기되어 (decf acc n) 문구가 수행되고, acc가 10에서 3 감소한다.
  4. (alet-test 'invert) : 다시 'invert가 인자로 들어오면 두번째 if 문구의 then절로 분기되어 (setq this #'self) 문구가 수행되고, this가 다시 alambda의 람다 함수로 바뀐다.
  5. (alet-test 5) : 다시 숫자 5으로 this가 가리키는 람다 함수로 호출이 전달되어 첫 번째 if 문구의 else 절로 분기되어 (incf acc n) 문구가 수행되고, acc가 7에서 5 증가한다.

alet 매크로에서 this 심볼이 2개의 lambda 함수에 교대로 바인딩되고 있는데, 사실 바인딩되는 함수에는 제한이 없다.

alambda를 매크로 확장하면 우리는 약간의 재밌는 힌트를 얻을 수 있다.

> (macroexpand
   '(alambda (n)
      (if (eq n 'invert)
        (setq this
              (lambda (n)
                (if (eq n 'invert)
                  (setq this #'self)
                  (decf acc n))))
        (incf acc n))))
 
(LABELS ((SELF (N)
          (IF (EQ N 'INVERT)
            (SETQ THIS
                  (LAMBDA (N)
                    (IF (EQ N 'INVERT)
                      (SETQ THIS #'SELF)
                      (DECF ACC N))))
            (INCF ACC N))))
  #'SELF)

alambda를 확장하면 사실 내부적으로 label을 사용하고 있음을 알 수 있다. alet이 this 심볼을 교대로 2개의 함수에 바인당한다는 것을 감안해 본다면, 위 매크로 확장은 다음과 같이 리팩토링할 수 있다.

(alet ((acc 0))
  (labels ((going-up (n)
             (if (eq n 'invert)
               (setq this #'going-down)
               (incf acc n)))
           (going-down (n)
             (if (eq n 'invert)
               (setq this #'going-up)
               (incf acc (- n)))))
    #'going-up))

macrolet은 let과 같은데 단지 지역 매크로를 정의하는 것이다. 위 코드에서 (setq this …) 코드는 macrolet을 이용하여 더 줄일 수 있다.

(defmacro alet-fsm (&rest states)
  `(macrolet ((state (s)
                `(setq this #',s)))
     (labels (,@states) #',(caar states))))
(alet ((acc 0))
  (alet-fsm
    (going-up (n)
      (if (eq n 'invert)
        (state going-down)
        (incf acc n)))
    (going-down (n)
      (if (eq n 'invert)
        (state going-up)
        (decf acc n)))))

alet-fsm 매크로는 상태를 정의하는 복수의 문구를 states 파라미터로 받는다. 이 states 파라미터는 매크로의 (labels (,@states) #',(caar states))를 통해 문구에서 지역 함수들을 정의한 후 마지막 문구로 정의된 함수를 리턴한다.

반면 상태를 정의하는 각 문구에는 state라는 자유 변수가 사용되었는데, 이 자유 변수는 alet-fsm 매크로의 (macrolet 3))…) 문구를 통해 this 자유 변수에 s 파라미터로 받은 심볼을 지정하도록 정의된다.

anaphor injection

alet 매크로로 도입되는 자유변수 this가 보이지 않고, 또한 alet-fsm 매크로에서 this 자유 변수를 사용하는 것도 보이지 않는다. 즉 alet-fsm 매크로는 this라는 anaphor 자유 변수를 주입하고 있다.

6.4 Indirection Chains

alet 매크로가 하는 일은 다음과 같이 요약할 수 있다.

  1. this가 alet 매크로의 body의 마지막 문구(form) (대개 lambda form)를 평가한 결과(클로져)를 가리킨다.
  2. alet 매크로의 body의 나머지 문구들이 먼저 수행된다.
  3. 최종적으로 this가 가리키는 람다함수를 호출하는 더미 클로져를 리턴한다.

이 더미 클로져가 alet 매크로가 평가된 최종 결과물이다. this가 가리키는 실제 클로져는 그 환경을 같이 공유한다. 이 환경에 this라는 변수가 있는 것인데, 이 this를 이용하는 여러 가지 방법이 있다.

ichain-before 매크로

(defmacro! ichain-before (&rest body)
  `(let ((,g!indir-env this))
     (setq this
       (lambda (&rest ,g!temp-args)
         ,@body
         (apply ,g!indir-env
                ,g!temp-args)))))

ichain-before 매크로는 alet 매크로 안에서 사용되기 위한 매크로인데, alet의 더미 클로져가 호출되기 전에 수행되는 코드를 추가한다.

다음은 ichain-before 매크로가 alet 매크로와 함께 사용되는 사용예를 보인다.

> (alet ((acc 0))
    (ichain-before
      (format t "Changing from ~a~%" acc))
    (lambda (n)
      (incf acc n)))
> (funcall * 2)
Changing from 0
2
> (funcall ** 2)
Changing from 2
4

ichain-before 매크로의 평가

ichain-before 매크로의 정의를 보자. 이 매크로는 새로운 let 환경을 도입하고 있다. 그리고 this를 ,g!indir-env라는 지역 변수에 할당하고 있다. 그리고 setq this를 통해 this에 새로운 더미 클로져를 할당한다.

ichain-before 매크로가 하는 일을 다시 한 번 정리하면 다음과 같다.

  1. 새로운 let 환경을 도입하여 기존에 this가 가리키던 클로져를 이 환경의 ,g!indir-env 변수에 저장한다.
  2. 이 let 환경을 기반으로 하는 새로운 더미 클로져를 리턴한다.
  3. 이 더미 클로져는 ichain-before 매크로의 body를 수행한 후 alet의 더미 클로져(,g!indir-env 지역 변수에 저장된)를 호출한다.

위의 ichain-before 매크로가 alet 매크로에서 사용된 예를 확장하면 다음과 같다.

(LET ((THIS) (ACC 0))
  (SETQ THIS
          (LAMBDA (N)
            (LET* ((#:G1128 N) (#:NEW1127 (+ ACC #:G1128)))
              (SETQ ACC #:NEW1127))))
  (LET ()
    (LET ((#:INDIR-ENV1129 THIS))
      (SETQ THIS
              (LAMBDA (&REST #:TEMP-ARGS1130)
                (FORMAT T "Changing from ~a~%" ACC)
                (APPLY #:INDIR-ENV1129 #:TEMP-ARGS1130)))))
  (LAMBDA (&REST PARAMS) (APPLY THIS PARAMS)))

2개의 let 절

  • 첫번째 let 절은 alet 매크로에 의해 도입된 것이다.
  • 두번째 let 절은 ichain-before 매크로에 의해 도입된 것이다.

첫번째 let 절이 수행될 때 this는 alet 매크로의 실제 클로져를 가리키게 된다. 이것은 (incf acc n)을 수행한다. 두번째 let 절이 수행될 때 this가 가리키던 alet 매크로의 실제 매크로는 #:INDIR-ENV1129 변수에 할당되고, this는 ichain-before의 더미 클로져를 가리키게 된다.

최종적으로 리턴된 alet 더미 클로져가 호출될 때 this가 가리키는 클로져는 ichain-before의 더미 클로져이다. 이 더미 클로져는 ichain-before 매크로의 body를 수행한 후 자신의 let절의 #:INDIR-ENV1129 변수에 저장된 alet의 실제 클로져를 호출한다.

매크로 확장을 다시 해석해 보면 this가 새로운 let절이 도입되면서 새로운 클로져를 가리키도록 이동하고 있다는 사실을 알 수 있다.

처음에 this는 alet의 실제 클로져를 가리키다 다음에는 ichain-before의 더미 클로져를 가리킨다.

첫번째 let 절 :
this                 ==> alet의 실제 클로져

두번째 let 절 :
#:INDIR-ENV1129      ==> alet의 실제 클로져
this                 ==> ichain-before의 더미 클로져

호출은 다음과 같은 흐름을 따라 간다.

alet의 더미 클로져 ==> this ==> ichain-before의 더미 클로져 ==> alet의 실제 클로져

ichain-before 매크로가 없었다면 원래 호출 흐름은 다음과 같았을 것이다.

alet의 더미 클로져 ==> this ==> alet의 실제 클로져

ichain-before 매크로는 this가 자신의 더미 클로져를 가리키도록 한 후, this가 원래 가리키는 클로져를 호출하고 있다.

이처럼 ichain-before 매크로는 alet 매크로의 this의 참조를 자신의 것으로 교체하는 동작을 하는 것인데, 이러한 동작은 여러 번 연속해서 수행될 수 있다. (사실 그래서 chain이라는 말을 사용한 것이다)

> (alet ((acc 0))
    (ichain-before
      (format t "A~%"))
    (ichain-before
      (format t "B~%"))
    (ichain-before
      (format t "C~%"))
    (lambda (n)
      (incf acc n)))

이 매크로를 확장하면 다음과 같다.

(LET ((THIS) (ACC 0))
  (SETQ THIS
          (LAMBDA (N)
            (LET* ((#:G1134 N) (#:NEW1133 (+ ACC #:G1134)))
              (SETQ ACC #:NEW1133))))
  (LET ()
    (LET ((#:INDIR-ENV1135 THIS))
      (SETQ THIS
              (LAMBDA (&REST #:TEMP-ARGS1136)
                (FORMAT T "A~%")
                (APPLY #:INDIR-ENV1135 #:TEMP-ARGS1136)))))
  (LET ()
    (LET ((#:INDIR-ENV1137 THIS))
      (SETQ THIS
              (LAMBDA (&REST #:TEMP-ARGS1138)
                (FORMAT T "B~%")
                (APPLY #:INDIR-ENV1137 #:TEMP-ARGS1138)))))
  (LET ()
    (LET ((#:INDIR-ENV1139 THIS))
      (SETQ THIS
              (LAMBDA (&REST #:TEMP-ARGS1140)
                (FORMAT T "C~%")
                (APPLY #:INDIR-ENV1139 #:TEMP-ARGS1140)))))
  (LAMBDA (&REST PARAMS) (APPLY THIS PARAMS)))

매크로 확장을 보면 let 문구의 수 만큼 this는 계속 다른 클로져를 가리키도록 바뀌고 있다. 최종적으로는 다음과 같이 된다.

#:INDIR-ENV1135  ==> 첫번째 let 문구의 클로져(즉 alet 매크로의 실제 클로져)
#:INDIR-ENV1137  ==> 두번째 let 문구의 클로져(#:INDIR-ENV1136)
#:INDIR-ENV1139  ==> 세번째 let 문구의 클로져(#:INDIR-ENV1137)
this             ==> 마지막 let 문구의 클로져

최종적으로 리턴된 alet 매크로의 더미 클로져의 수행은 다음과 같다.

alet의 더미 클로져 ==> this ==> #:INDIR-ENV1139 ==> #:INDIR-ENV1137 ==> #:INDIR-ENV1135 ==> alet의 실제 클로져

그래서 호출은 ichain-before가 선언된 것과는 역순으로 수행된다.

> (funcall * 2)
C
B
A
2

Indirection Chain의 동적 추가

실행 시간에 새로운 클로져를 동적으로 추가하고, this를 통해 이 클로져의 환경에 접근할 수 있기 때문에 ichain-before 매크로를 동적으로 사용할 수 있다.

다음은 클로져가 호출될 때마다 새로운 코드를 추가하는 ichain-before의 사용예이다.

(alet ((acc 0))
    (lambda (n)
      (ichain-before
        (format t "Hello world~%"))
      (incf acc n)))

여기서는 ichain-before 매크로가 alet 매크로의 더미 클로져안에 있다. 이것을 확장하면 다음과 같다.

(LET ((THIS) (ACC 0))
  (SETQ THIS
          (LAMBDA (N)
            (LET ()
              (LET ((#:INDIR-ENV1143 THIS))
                (SETQ THIS
                        (LAMBDA (&REST #:TEMP-ARGS1144)
                          (FORMAT T "Hello world~%")
                          (APPLY #:INDIR-ENV1143 #:TEMP-ARGS1144)))))
            (LET* ((#:G1146 N) (#:NEW1145 (+ ACC #:G1146)))
              (SETQ ACC #:NEW1145))))
  (LAMBDA (&REST PARAMS) (APPLY THIS PARAMS)))

이 매크로의 확장이 평가되면 alet 매크로의 더미 클로져의 this는 첫번째 람다 함수가 된다. 이것은 alet 매크로의 실제 클로져이다. 그런데 실제 클로져가 하는 일은 this를 ichain-before 매크로의 더미 클로져로 바꾸고 이전 실제 클로져는 #:INDIR-ENV1143에 의해 참조되도록 바꾼 후 acc를 증가 하는 것이다.

만약 이 상태에서 alet의 더미 클로져가 다시 실행되면 이 때의 this는 ichain-before의 더미 클로져이기 때문에 두번째 람다 함수가 수행되는데 이 함수는 “Hello world”를 찍고나서 원래 클로져인 alet의 실제 클로져를 수행한다. alet이 실제 클로져가 수행되면 현재 this가 가리키는 것을 새로운 let 문구의 환경에서의 #:INDIR-ENV1143가 가리키도록 대체되고, this는 다시 새로운 let 문구의 환경의 클로져를 가리키게 된다.

다시 또 alet의 더미 클로져가 수행되면, this는 ichain-before의 신규 클로져를 호출하여 “Hello world”가 찍히고, 이 신규 클로져의 환경의 #:INDIR-ENV1143 가 수행되는데, 이는 다시 이전 ichain-before의 이전 클로져를 수행하여, “Hello world”가 수행되고, 또 이전 클로져의 환경의 #:INDIR-ENV1143가 수행되면서 alet의 실제 클로져가 수행된다.

그 결과는 다음과 같다.

* (loop for i from 1 to 4
    do
      (format t "~:r invocation:~%" i)
      (funcall * i))
first invocation:
second invocation:
Hello world
third invocation:
Hello world
Hello world
fourth invocation:
Hello world
Hello world
Hello world

다시 정리해 보자.

alet의 더미 클로져가 최초 실행시의 간접 참조 사슬 (Indirection Chain)

 alet 더미 클로져 ==> this ==> alet의 실제 클로져   
 

alet의 실제 클로져는 새로운 let 문구를 만들고 this가 가리키던 alet의 실제 클로져는 #:INDIR-ENV1143가 참조하도록 바꾸고, this는 ichain-before 매크로의 더미 클로져를 참조하도록 바꾼다. 다음과 같다.

alet의 더미 클로져가 최초 실행 결과

 this            ==> ichain-before의 더미 클로져
 #:INDIR-ENV1143 ==> alet의 실제 클로져

alet 더미 클로져의 첫 실행 결과는 두번째 실행에서 그 상태가 된다.

alet의 더미 클로져가 최초 실행시의 간접 참조 사슬 (Indirection Chain)

 alet 더미 클로져 ==> this ==> ichain-before의 더미 클로져 ==> #:INDIR-ENV1143 ==> alet의 실제 클로져
1)
dlambda…
2)
funcall this :reset
3)
state (s) `(setq this #',s
study/anaphoric_macros.1403393041.txt.gz · Last modified: 2019/02/04 14:26 (external edit)