; 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이 다르게 캡처링 되면서 리턴값이 달라진다.
2019년 9월 1일 일요일
[on lisp] 9. 매크로 캡처링문제
피드 구독하기:
댓글 (Atom)
댓글 없음:
댓글 쓰기