2019년 9월 1일 일요일

[on lisp] 9. 매크로 캡처링문제

; 9.1 variable capture

;wrong
(defmacro for ((var start stop) &body body)
    `(do ((,var ,start (1+ ,var))
          (limit ,stop))
         ((> ,var limit))
         ,@body))

; seems work fine
(for (x 1 5)
     (princ x))
;error 이렇게 limit을 넣으면 문제가 된다.
;(for (limit 1 5)
;     (princ limit))
;why
; (> limit limit) 이름 충돌 여기도 변수충돌이 일어난다.
;(do ((limit 1 (1+ limit))
;     (limit 5))
;    ((> limit limit))
;    (princ limit))

; 9.2 Free Symbol Capture
; 덜 빈번하게, 매크로 정의 자체에서 매크로가 확장 될 때 environment에 실수로 바인딩을 하는 기호를 포함할 수도 있다. (환경변수가 어쩌다 걸려버린것)
; 어떤 프로그램을 소개한다, warning을 출력하는 대신, 나중에 확인하려고 리스트에 add하기를 원한다고 해보자.
; 한 개발자가 매크로 gripe를 작성한다. 경고를 글로벌 리스트(w)에 넣는 다.
(defvar w nil)

(defmacro gripe (warning)
    `(progn (setq w (nconc w (list ,warning)))
         nil))

; 그런 다음 다른 사람이 sample-ratio라는 함수를 만들고 그 안에 gripe를 넣었다.
; 이 함수는 매개변수 v,w의 길이의 비율을 확인한다. 거기서 비율이 2보다 작으면 해당 내용을 로그리스트(w)에 추가하고
; nil을 리턴한다. 아래 코드를 보다.

(defun sample-ratio (v w)
  (let ((vn (length v)) (wn (length w)))
    (if (or (< vn 2) (< wn 2))
        (gripe "sample < 2")
        (/ vn wn))))
; [sample-ratio]가 w=(b)로 호출된다면, w의 매개변수는 2개 이하 이기 때문에 워닝을 할 것이다.
; 하지만 gripe이 확장되는 순간, sample-ratio 안에 정의된 것처럼 보이게 된다.

(defun sample-ratio (v w)
  (let ((vn (length v)) (wn (length w)))
     (if (or (< vn 2) (< wn 2))
         (progn (setq w (nconc w (list "sample < 2")))
                 nil)
         (/ vn wn))))
; 문제는 gripe매크로가 sample-ratio안으로 들어가면서 w가 글로벌 변수 w를 쓰는 것이 아니라.
; sample-ratio안에 있는 로컬변수에 바인딩 되는 것이다. 원래는 free variable이 되서 글로벌로 범위가 커지는 것인데
; 이제는 free-variable이 아니라 로컬변수처럼 된 것이다.
; warning은 이제 글로벌 리스트에 저장되는 것이 아니라, 로컬변수에 들어간다.
; 이제는 warning출력값만 잃은 것이 아니라. 매개변수로 들어온 W=(b)값 마저 잃게 된다.
(print (let ((lst '(b)))
  (sample-ratio nil lst)
   lst))
; (B "sample < 2") lst가 이렇게 바뀐 것이다.
(print w)
; nil : w에는 아무것도 들어가지 않았음


; 9.3 When Capture Occurs (언제 캡처가 일어나나)
; 변수 캡처는 미묘한 문제이며 캡처 가능한 기호가 프로그램에서 장난을 일으킬 수있는 모든 방법을 예상하려면 약간의 경험이 필요하다.
; 다행스럽게도, 캡처로 인해 프로그램이 잘못 될 수 있는 방법에 대해 생각 할 필요없이 매크로 정의에서 캡처 가능한 기호를 감지하고 제거 할 수 있다.
; 캡처 가능한 변수를 정의하는 규칙은 먼저 정의해야하는 일부 하위 개념에 따라 다릅니다.

; Free : 기호 [s]는 해당 표현식에서 변수로 사용될 때 표현식에서 사용 가능하지만 표현식은 이에 대한 바인딩을 작성하지 않습니다.
; s가 변수로 들어오면 사용하지만, 표현식 안에서 바인딩을 생성 하지는 말라는 말인가. 아래를 보자
; (let ((x y) (z 10))
;   (list w z x))
; w,x,z모두 list표현식에서는 free variable이다, 바인딩이 없는 것이다.
; 하지만 let으로 둘러싸면 x,z에 대한 바인딩을 적용한다. 좋아 let 안 전체를 보면 y,w는 free variable이 되는 것이다.
; 또 알아야 하는 것은
; (let ((x x))
;   x)
; 여기서 (x x)가 있는데 두번째 x만 free다 이 녀석은 x를 위해 바인딩 된 새로운 스코프에서 온 녀석이 아니다.

; Skeleton : 매크로 확장의 골격은 전체 표현식이며, 매크로 호출에서 매개변수 부분을 뺀 것이다.
(defmacro foo (x y)
  `(/ (+ ,x 1) ,y))
; 그리고 호출해보자
(print
  (macroexpand-1
    '(foo (- 5 2) 6)))
; (/ (+ (- 5 2) 1) 6) 
; 여기서 스켈레톤은 확장된 전체 표현식 중에서 파라미터 x y를 뺀 녀석이다.
; (/ (+         1)  )
; 이 두 가지 개념을 정의하면, 캡쳐 가능한 기호를 탐지하기 위한 간결한 규칙을 진술하는 것이 가능하다.

; Capturable : 심볼은 몇몇 매크로 확장에서 캡처될 수 있다.
; (a) free variable이 매크로 확장의 스켈레톤 안에 있던가
; (b) 매개변수로 들어온 변수가 (바인딩하거나 평가하는) 스켈레톤의 부분에 바인딩 되는 경우 (덮어씌어지는 경우)
(defmacro cap1 ()
  '(+ x 1))
; 여기서 x는 capturable이다. 왜냐하면 free variable이 스켈레톤 안에 있다. gripe에서도 본 버그이다.
(defmacro cap2 (var)
  `(let ((x ...)
         (,var ...))
     ...))
; 여기서 x는 capturable이다. x가 매개변수로 들어오는 x도 바인딩 해버린다. (9.1 for에서 확인했다.)
; 반면에 아래 두 매크로를 보자.
(defmacro cap3 (var)
  `(let ((x ...))
     (let ((,var ...))
       ...)))

(defmacro cap4 (var)
  `(let ((,var ...))
     (let ((x ...))
       ...)))
; 모두 x가 capturable이다. 하지만, x가 바인딩 된 곳 안에 문맥(context)가 없고, 매개변수로 들어온 녀석이 없다면, cap3/cap4 모두 사용할 수 있다.
(defmacro safe1 (var)
  `(progn (let ((x 1))
            (print x))
          (let ((,var 1))
            (print ,var))))
; 여기서 x는 capturable이 아니다. 
; 잘보면! let를 쓰고 그 안에서 x만 쓴다. 매개변수 var를 쓰고 있지 않다. 게다가 환경변수 같이 밖에 있는 문맥(context)를 끌어들이지 않았다.
; 이처럼 모든 스켈레톤 안에 있는 bound variables가 위험하지는 않다.
; 하지만 만약 매개변수가 스켈레톤에 의해 설정된 바인딩 내에서 평가되는 경우는,
(defmacro cap5 (&body body)
  `(let ((x ...)
     ,@body))
; 그러면 바인딩된 변수는 캡처링 될 위험이 있다. cap5에서 x는 캡처위험이 있다.
(defmacro safe2 (expr)
  `(let ((x ,expr))
     (cons x 1)))
; x는 캡처위험이 없다, 왜냐하면 expr에 전달된 매개변수를 평가할 때, 새로 바인딩 된 x는 보이지 않기 때문이다.
; 또한 우리가 걱정해야 하는 것은 the binding of skeletal variables(스켈레톤 변수의 바인딩)이다. 아래 매크로를 보자.
(defmacro safe3 (var &body body)
  `(let ((,var ...))
     ,@body))
; 어떤 심볼도 의도치 않게 갭처될 위험이 없음.
; 이제 캡처 가능한 기호를 식별하는 새로운 규칙에 비추어 'for'의 원래 정의를 살펴보자.
(defmacro for ((var start stop) &body body) ;; wrong
  `(do ((,var ,start (1+ ,var))
        (limit ,stop))
       ((> ,var limit))
     ,@body))
; 이제 위에 정의는 두 가지 방식으로 취약하는 것을 알게 되었다.
; 1. limit 이 for의 첫번째 매개변수로 들어올 수 있다.
(for (limit 1 5)
  (print c limit))
; 2. 그런데 limit이 loop의 body에 들어가는 것도 위험하다.
(let ((limit 0))
  (for (x 1 10)
    (incf limit x))
  limit)
; 여기서 for문은 끝나지 않을 것이다.
; 이 섹션에서 보여준 룰들로 전부 잡을 수 있는 것은 아니다. 
; capture문제는 보는 것에 따라 모호하게 정의된다. 예를 즐다
(let ((x 1)) (list x))
; 우리는 (list x)를 평가할 때 그게 오류라고 하지는 않는다. x가 새로운 변수에 들어간다고 문제라고 생각하지는 않을 것이다.
; 캡처링 문제를 알아내는 룰도 부정확하다.
; 이런 캡처링 룰을 패스하는 매크로를 만들 수 있지만, 그럼에도 의도치 않은 캡처링 문제를 직면할 수 있다.
; 아래 예를 보자.
(defmacro pathological (&body body)
  (let* ((syms (remove-if (complement #'symbolp)
                          (flatten body)))
         (var (nth (random (length syms))
                   syms)))
    `(let ((,var 99))
        ,@body)))
; 매크로가 호출 될 때, body 안에 표현식은 progn처럼 평가될 것이다.
; but one random variable within the body may have a different value.
; 하지만 body안에 있는 random 변수는 다른 값을 가질 것이ㅏ.

; 9.4 Avoiding Capture with Better Names (더 나은 이름으로 캡처링을 피한다?)
; 첫번째 두 섹션에서 캡처링을 두 가지 타입으로 나눴다.
; argument capture, 매크로 스켈레톤에 매개변수가 들어가는 거
; free symbol capture, free symbol이 매크로 확장 안에 있어서 예기치 못한 곳에 캡처링
; 이름을 *warning* 이렇게 글로벌로 넣으면서 캡처링이 되지 않도록 하자는 것 같다.
; 좋은 생각 같지는 않다.

; Avaoiding Capture by Prior Evaluation
; 때로 매개변수 캡처링은 매크로확장에서 만들어진 바인딩 밖에서 위험한 매개변수를 확장하면서 간단히 해결된다.
; Fig 9.1 Avoiding capture with let.
;; vulnerable to capture
(defmacro before (x y seq)
  `(let ((seq ,seq))
     (< (position ,x seq)
        (position ,y seq))))
; currect version
(defmacro before (x y seq)
  `(let ((xval ,x) (yval ,y) (seq ,seq))
     (< (position xval seq)
        (position yval seq))))

; 9.2 Avoiding capture with a closure.
; vulnerable to capture
(defmacro for ((var start stop) &body body)
  `(do ((,var ,start (1+ ,var))
        (limit ,stop))
       ((> ,var limit))
     ,@body))
; correct version
(defmacro for ((var start stop) &body body)
  `(do ((b #'(lambda (,var) ,@body))
        (count ,start (1+ count))
        (limit ,stop))
       ((> count limit))
     (funcall b count)))
; 이렇게 사용할 녀석들을 전부 let으로 다시 바인딩해서 사용하는 걸 원하나보다.


; 9.6 Avoiding Capture with Gensyms * (gensyms이용하기)
; gensyms는 같은 값이 없는 것을 약속하는데
; 이 건 각 패키지에서 모든 심볼의 이름을 추적한다.
; 이 심볼들은 패키지 안에 interned된다고 말한다.
; gensym을 호출하면 uninterned unique한 심볼을 리턴한다.
; 그러므로 
(eq (gensym) ...)
; 이렇게 테스트를 해보려고 하면 절대 일어날 수 없는 일이 될 것이다.
(gensym) ; #G:47 뭐 이런식으로 보여준다. 이름을 딱히 중요하지 않다. 

;9.3 Avoiding capture with gensym.
(defmacro for ((var start stop) &body body)
  `(do ((,var ,start (1+ ,var))
        (limit ,stop))
       ((> ,var limit))
     ,@body))
; A correct version
(defmacro for ((var start stop) &body body)
  (let ((gstop (gensym)))
    `(do ((,var ,start (1+ var))
          (,gstop ,stop))
         ((> ,var ,gstop))
       ,@body)))
; 그림 9.3을 보면 gensyms를 이용한 정의가 있다.
; 이제 limit 혹은 stop 뭐 이런걸로 심볼이 충돌나지는 않을 것이다.

; 9.7 Avoiding Capture with Packages. (패키지로 캡처링 피하기)
; 어느정도는 매크로를 패키지 않에 넣는 것으로 캡처링을 피하는 것이 가능하다.
; 만약 매크로들의 패캐지를 따로 만들어서(macros라고 하자) 그 안에 for매크로를 만든다면 초반에 만든 매크로 마저 쓸 수 있게 된다.
(defmacro for ((var start stop) &body body)
  `(do ((,var ,start (1+ ,var))
        (limit ,stop))
       ((> ,var limit))
     ,@body))
; 같은 패키지는 아니고 다른 패키지에서 사용한다고 해보자.
; mycode라는 패키지에서 macros패키지를 사용한다고 해보자.
; limit이라는 이름으로 첫번째 매개변수를 사용한다 하더라도!
; mycode:limit이 되고 macros::limit이 되어 충돌이 되지 않을 것이다.
; 문제는 뭐냐 1. 패키지는 이런식으로 쓰라고 만들어 진 것이 아니다.
; 2. 같은 패티지에서는 문제가 생긴다.

; 9.8 Capture in Other Name-Spaces (다른 네임스페이스에서의 캡처)
; 지금까지는 variable capture이지만, 커먼리습에서는 name-space가 문제될 수 있다.
; 함수도 또한 로컬에서 바인딩 될 수 있으며, 함수 바인딩은 변수바인딩과 같이 문제를 일으키기 쉽다.
(defun fn (x) (+ x 1))
(defmacro mac (x) `(fn ,x))
(mac 10) ;; 11
(labels ((fn (y) (- y 1)))
  (mac 10) ;; 9
; 위치에 따라 fn이 다르게 캡처링 되면서 리턴값이 달라진다.

댓글 없음:

댓글 쓰기