2019년 9월 11일 수요일

[on lisp] 18. destructuring - 1

이 매크로를 이해하는데 꽤나 오랜 시간이 걸렸다...
정말 폴그레이엄 미친 사람이다. 어떻게 이런책을 쓰고 이런 기술을 무료로 풀 수가 있는 거지?
이정도의 기술은 아무것도 아니라는 것인가?

그 머리를 가지고 싶다.
다른 onlisp내용은 스킵을 해도 이렇게 어렵게 이해한 내용은 적어놔야겠다.
커먼리습으로 평생 코딩을 하지 않더라도 이런 내용을 익힌다는 것은 참으로 재미 있다.

한가지 문제는 챕터 18. destructuring이 이제 시작이라는 점이다.
이 챕터는 특이하게 내용이 너무 많다...
;; '(dbind (a b c) (list 1 2 3) (list a b c))
;; pat = (a b c), seq = (list 1 2 3), body = (list a b c)
;; 바인딩할 값들을 gseq로 설정한 후 
;; dbind-ex으로 표현식을 만드는데, 바인딩하는 형태는 destruc가 한다.
(defmacro dbind (pat seq &body body)
  (let ((gseq (gensym)))
    `(let ((,gseq ,seq))
       ,(dbind-ex (destruc pat gseq #'atom) body))))

; let 안에 들어갈 리스트를 생성한다.
; 1. pat이 없으면 nil를 던진다(재귀의 베이스케이스) 아마 (cons ... nil)로 생성될듯
; 2 rest를 바인딩한다. 자세한 내용은 아래 잇음.
; 3 rest가 하나만 리스트가 아닌 경우만 rest가 걸리는데
;   걸리면 `((,rest (subseq ,seq ,n))) 이거로 한번에 다! 연결해버린다. rest가 원래 그런거니까 다음 내용 다 바인딩하는 녀석
;   ((( (subseq (list 1 2 3) 1)) => ((A (2 3)) 
; 4.rest가 없으면 일반적인 바인딩이 시작된다. (재귀 시작)
; 5. 이제 (car pat)로 첫번째 심볼을 꺼낸다.
; 6. (rec (destruc (cdr pat) seq atom? (1+ n))) 로 재귀로 다음 녀석들을 재귀로 만들어 낸다.
;    그 다음에 뭐 일단 첫번째 녀석을 먼저 손봐보자 재귀를 하는 곳이 6번만이 아니다.
; 7. p가 하나가 아니라면? 첫번째 가져온 바인딩될 녀석이 또! 리스트라면? (destructuring중이다)
; 8. 일단 p가 하나인 경우를 보자. 
; 9. (cons `(,p (elt ,seq ,n)) rec) 중요한 것은 (cons A rec) 에서 A다 여기가 이번 호출에서 하는 일이다.
; (,p (elt ,seq ,n)) 라고 해보자. 첫번째 재귀라면 (A (elt (list 1 2 3) 0)) -> (A 1) 이렇게 될 것이다. 
; 두번째는 (B (elt (list 1 2 3) 1)) -> (B 2)가 될 것이다. elt가 위치의 값을 가져온다.
; 10. 그 값을 cons로 뒤에 재귀값과 더하는 것이다.
; 11. 드디어 왔다. 만약 p가 리스트라면!!
; 여기 새로운 seq를 바인딩하여 캡처링을 보호해야 한다. (var (gensym)) 으로 임의의 심볼을 만든다.
; 12. (cons (cons `(,var (elt ,seq ,n))
;                  (destruc p var atom?))
;           rec
; cons가 두개 인걸 보니 바인딩 리스트 안에 리스트를 또 만든 듯. 
; `(,var (elt ,seq ,n))는 (#:G2 (elt seq n))으로 현재 위치에 있는 바인딩 값을 바인딩 해서 재귀로 쓸 때 보낸다. 
; 아래테스트 코드에도 설명이 되어 있다.
(defun destruc (pat seq &optional (atom? #'atom) (n 0))
  (if (null pat) ; 1
      nil
      (let ((rest (cond ((funcall atom? pat) pat) ; 2 처음들어오면 여긴 아님.
                        ((eq (car pat) '&rest) (cadr pat)) ; 3 일단 예시에서는 없음
                        ((eq (car pat) '&body) (cadr pat)) ; 4 여기도 아님.
                        (t nil)))) ; 대부분 리스트로 들어올테니 여기 nil이 된다.
       (if rest ; pat가 atom 이거나 &rest,&body가 아니라 '(a b c)로 들어오면 nil임.
           `((,rest (subseq ,seq ,n))) ; ((A (subseq (list 1 2 3) 1)) => ((A (2 3)) 
           (let ((p (car pat)) ; 4.5 리스트의 첫번째 녀석을 
                 (rec (destruc (cdr pat) seq atom? (1+ n)))) ; 6
             (if (funcall atom? p) ; 
                 (cons `(,p (elt ,seq ,n)) ;9  여기가 중요하다. p는 파라미터 심볼 하나고 elt로 seq의 n번재 녀석을 가져온다는 뜻이다. 그러므로 여기서는 바인딩만 가져오는 것.
                       rec) ; 10
                 (let ((var (gensym))) ; 11
                   (cons (cons `(,var (elt ,seq ,n)) ; 12
                               (destruc p var atom?)); 
                         rec))))))))

; destruc로 만든 코드를 dbind-ex에 binds라는 이름으로 들어온다.
; 바인드가 있으면,
; (let ...)안에 들어가게 되는데 바로 들어가는 것은 아니고
; #'(lambda (b) (if (consp (car b)) (car b) b)) 를 한번씩 거친다.
; (car b)가 리스트면 그대로 보내고 (car b)가 리스트가 아니라면 b를 놓는다.(리스트 형태를 그대로 유지한다. 해당 심볼은 nil이 될듯 바로 (b nil)로 바인딩된다.
; 왜냐하면 (b nil) == (cons b (cons nil))이니까...
; 특이한 점은 리스트가 여기서 다 바인딩 되는 것이 아니다. 바깥에 바인딩 된 녀석들만 바인딩이 되는데
; 중요한 것은 여기서 바인딩 안에 들어갈 새로운 seq의 gensym 값도 바인딩이 된다. 
; 그리고 그녀석이 다음 재귀에서 만들어질 녀석들이 사용할 seq값이다.
; 다 돌면 body를 푼다.
(defun dbind-ex (binds body)
  (if (null binds)
      `(progn ,@body)
      `(let ,(mapcar #'(lambda (b)
                         (if (consp (car b))
                             (car b)
                             b))
                     binds)
        ,(dbind-ex (mapcan #'(lambda (b)
                               (if (consp (car b))
                                   (cdr b)))
                           binds)
                   body))))


;(print
; (macroexpand '(dbind (a b c) (list 1 2 3) (list a b c))))
;(LET ((#:G3210 (LIST 1 2 3)))
; (LET ((A (ELT #:G3210 0)) (B (ELT #:G3210 1)) (C (ELT #:G3210 2)))
;  (PROGN (LIST A B C))))
; #:G3210 binding에 연결.
; (ELT가 뭐지... ELT로 해당 앨리먼트의 0번째 1번째 2번째를 가져오는 것 같다.)
; 그 후 (PROGN에 바디를 바인딩한다.

;(print
; (destruc '(a b c) (list 1 2 3) #'atom)
; )
; ((A (ELT (1 2 3) 0)) (B (ELT (1 2 3) 1)) (C (ELT (1 2 3) 2))) 


; 리스트 안에 리스트가 있는 경우
'(print
 (macroexpand '(dbind (a (b c)) (list 1 (list 2 3)) #'atom)))
;(LET ((#:G3210 (LIST 1 (LIST 2 3))))
; (LET ((A (ELT #:G3210 0)) (#:G3211 (ELT #:G3210 1)))
;  (LET ((B (ELT #:G3211 0)) (C (ELT #:G3211 1))) (PROGN #'ATOM)))) 
;이걸보면 알 수 있는 것이. 바인딩 되는 심볼이 깊이가 있으면 그 갯수만큼 (gensym)이 있게 된다.
;그 이유는 바인딩할 seq(list 1 (list 2 3))이 달라지기 때문이다.
;위 코드에서 두번째 코드가 신기한데. 일단 새롭게 바인딩할 값 또한 리스트의 리스트로 들어가야 하는 상황인 것이다.
;(#:G3211 (ELT #:G3210 1))
;이 코드인데 (#:G3211 (elt (list 1 (list 2 3)) 1)이 되는 것이다.
;그러면 (#:G3211 (list 2 3))이 되면서 리스트안으로 들어가게 된다.
;

'(print
 (destruc '(a (b c)) (list 1 (list 2 3)) #'atom))
; ((A (ELT (1 (2 3)) 0))
; ((#:G3210 (ELT (1 (2 3)) 1)) (B (ELT #:G3210 0)) (C (ELT #:G3210 1)))) 

(print
 (dbind-ex '((A (ELT (1 (2 3)) 0))
              ((#:G3210 (ELT (1 (2 3)) 1)) (B (ELT #:G3210 0)) (C (ELT #:G3210 1)))) 
            (list 1 (list 2 3))))
;(LET ((A (ELT (1 (2 3)) 0)) (#:G3210 (ELT (1 (2 3)) 1)))
; (LET ((B (ELT #:G3210 0)) (C (ELT #:G3210 1))) (PROGN 1 (2 3)))) 

(print
 (dbind-ex '((A (ELT (1 2 3) 0)) (B (ELT (1 2 3) 1)) (C (ELT (1 2 3) 2))) '(list a b c)))
;(LET ((A (ELT (1 2 3) 0)) (B (ELT (1 2 3) 1)) (C (ELT (1 2 3) 2)))
;  (PROGN LIST A B C)) 

;subseq가 뭔지도 몰랐다.
'(print (subseq (list 1 2 3) 0)) ; (1 2 3)
'(print (subseq (list 1 2 3) 1)) ; (2 3)
'(print (subseq (list 1 2 3) 2)) ; (3)


댓글 없음:

댓글 쓰기