Emacs Lisp에서의 lexcial scoping과 dynamic scoping

원문 : http://yoo2080.wordpress.com/2011/12/31/lexical-scoping-and-dynamic-scoping-in-emacs-lisp/

본 문에서 설명할 것들:

  1. Emacs Lisp에서의 lexcial scopingdynamic scoping의 차이점
  2. dynamic scoping에서 주의해야할 점
  3. lexical scopinglexical closures로 할 수 있는 것
  4. lexical scoping코드와 dynamic scoping코드를 섞을시 발생하는 일

Emacs Lisp는 Emacs 23(포함)이하 버전에서는 항상 dynamically scope였습니다. Emacs 24에서 lexical scoping의 지원이 추가되었습니다. 많은 이들이 대부분의 경우 dynamic scoping보다 **lexical scoping이 낫다는데 동의하기에, 상당히 멋진 일입니다. 왜 그런지에 대해서는 본문에서 곧 확인하실 수 있을 것입니다.

lexical scoping으로 블러오고자 하는 el파일이 있다면, 첫줄에 -*- lexical-binding: t -*-을 추가하기만 하면 됩니다. 그러면, Emacs 24가 파일을 읽을시, 그 el파일속 코드에 lexical scoping을 적용하게 됩니다.

예를들어, 현재 init 파일의 첫줄은 다음과 같습니다.

;; -*- coding: utf-8 -*-

그리고, 다음과 같이 바꾸겠습니다.

;; -*- coding: utf-8; lexical-binding: t -*-

이리하면, 제 init파일에 있는 코드는 Emacs24에서 lexically scope가 될 것입니다. 자세한 것은 file variables를 확인해 보시기 바랍니다.

lexcial scoping이 무엇인지 확인해보기 위해, 우선 빈 el파일을 만들어서 (C-x C-f lexical-scratch.el RET) 다음 라인을 추가해 봅시다:

;; -*- lexical-binding: t -*-

저장한 후, 버퍼를 돌려놓습니다 (M-x revert-buffer). 이제 저희는 lexical scoping에 놓인 scratch버퍼처럼 사용할 수 있습니다.

dynamic scopinglexical scoping은 무엇일까요? 간단한 예제 파일을 살펴보도록 하겠습니다.

(setq a 17)
(defun my-print-a ()
  (print a))
(setq a 1717)
(let ((a 8))
  (my-print-a))

my-print-aa가 정해지지 않았다는 사실을 주목하시기 바랍니다. 이러한 것을 것을 **"free variable"**이라 부릅니다.

위 코드를 돌려보면 결과가 어떻게 나올까요? 1717을 출력할까요? 아니면 8일까요? dynamic scoping이라면 8을 출력하고, lexical scoping이라면 1717을 출력합니다. dynamic scoping에선, my-print-a에 있는 amy-print-a호출될때(when my-print-a is called) 결정됩니다. lexical scoping에선 my-print-a가 **정의된 장소(where my-print-a is defined)**에 의해 결정됩니다.

dynamic scoping에서의 코드는 8을 출력하는데, my-print-a가 호출될시 a8로 local bind한 let form에 있기 때문입니다. let form이후에 my-print-a을 호출한다면, 1717을 출력할 것입니다..

lexical scoping에서의 코드는 1717을 출력합니다. 우선, my-print-a가 정의된 곳이 let form 밖이므로, my-print-a에 있는 a는 let form에서 생성된 local binding이 아닌 global bindinga를 참조하기 때문입니다. 다음으로, my-print-a가 호출될시, 8이된 **local value "a"**완 달리, **global value "a"**는 1717이기 때문입니다. 만약 my-print-a의 정의를 let form으로 옮긴다면, 출력된 값은 8이 될 것입니다. my-print-a에 있는 a가 let form에서 만들어진 local binding된 a를 참조할 것이기 때문입니다.

동일한 코드를 Javascript로 옮겨봤습니다.

var a;
a = 17;

function myPrintA() {
  console.log(a);
}


a = 1717;

(function () {
  var a = 8;
  myPrintA();
}());

위 코드는 1717을 출력합니다. 오늘날 프로그래밍 언어 대부분은 lexically scoped입니다.

여러분께서 Emacs 24를 사용하신다면, 다음 코드를 scratch버퍼에서 돌림으로써 1717을 출력하는 것을 확인해 볼 수 있습니다.

(eval
 '(progn
    (setq a 17)
    (defun my-print-a ()
      (print a))
    (setq a 1717)
    (let ((a 8))
      (my-print-a)))
 t)

Emacs 24에서의 eval함수는 두번째 인자(optional)를 받습니다. 이게 t라면, lexical scoping으로 평가합니다. (progn ...)앞에 '를 붙이는 것을 잊지 마시기 바랍니다.

Lexical scopinglecical closures를 가능케 합니다. 그렇다면 lexcial closure는 무엇일까요? 다음 코드를 살펴보도록 하겠습니다.

(setq a 0)
(let ((a 17))
  (defun my-print-a ()
    (print a))
  (setq a 1717))
(let ((a 8))
  (my-print-a))

lexical scoping에서, 위 코드는 1717을 출력합니다. 다음은 Alice가 생각한 것입니다:

처음에는 이상하지 않았는데, 다시보니 뭔가 좀 이상한데. 먼저, "lexical scoping 이므로, my-print-a에 있는 a가 첫번째 let form에의해 만들어진 local binding 을 참조해서 1717이 출력됬다" 라고 생각했지. 다시 살펴보니, my-print-a가 호출될시 첫번째 let form에 의해 만들어진 local binding이 만기될(expired)거라는 거야! 날짜 지난(expired) 우유를 먹을 순 없잖아! "미안, 나는 더이상 존재하지 않아" 대신 왜 1717이 출력된 거지? 에러는 커녕 lexcial scope로 돌아가는 이유가 뭐지?

첫번째 let form을 빠져나간 이후에도 첫번째 alocal biding은 살아남아, my-print-a가 접근하기만을 기다립니다. my-print-a를 제외하고 첫번째 alocal biding에 대한 모든 접근은 만료됩니다. 이는 Emacs는 뒤에서 여러가지 일들을 관리하며, lexical scoping은 **"기존것"**보다 더 많은 일을 할 수 있게되었다는 것을 의미합니다.

그러면, lexcial closure란 무엇일까요? 이는 "lexical scoping이 보다 더 많은 일을 할 수 있다"라는 것이 화면 뒤에서 어떻게 구현되는지와 관련있습니다. (symbol-function 'my-print-a)를 평가하면 볼 수 있는 것처럼, my-print-a의 함수 상자(function cell)은 a에 대한 만료된 binding에 대한 link를 담고 있습니다. 함수 정의와 함수 생성시 scope에 대한 link의 조합(combination)을 lexical closure 라 부릅니다. 혹은, 만료된 binding에 접근하는 lexically scoped 된 함수를 lexical closure 라 부를수도 있습니다. lexical closures 를 보통 단순하게 closures 라 불르기도 합니다. lexically scoped 언어들이 모두 closures 를 지원하는건 아닙니다.

lexical scoping 에서, 함수에 있는 a가 참조하는 것이 무엇인지 확인하기 위해선, 함수 본체 주변과 연관된 binding을 살펴보면됩니다. lexical scoping 은 코드에서 변수가 쓰여진 주변을 살펴보면 되기에 기억하기 쉬우며, 관계된 binding이 언제 만료되는지에 대해 걱정할 필요가 없습니다.

자, JavaScript로 된 코드를 살펴봅시다:

var a, myPrintA;
a = 0;
(function () {
  // local variable a
  var a = 17;
  myPrintA = function () {
    console.log(a);
  };
  a = 1717;
}());
(function () {
  // local variable a
  var a = 8;
  myPrintA();
}());

Javascript는 lexical closures 를 지원하기에 1717을 출력할 것입니다:

Emacs 24에서, lexically scoped (interpreted) 함수들은 (closure ENV ARGS BODY...)과 같은 함수 값 형태로 표현되는 반면, dynamically scoped 함수들은 익명함수(anonymous function)를 작성할때 사용한 것과 동일한 형태인 (lambda ARGS BODY...)와 같은 형태로 표현됩니다.

다음 코드는 dynamic scoping에서 (lambda (x y) (+ x y))을 두번 출력하는 코드입니다.

(defun my-sum (x y)
  (+ x y))
;; print the contents of function cell of my-sum
(print (symbol-function 'my-sum))
;; print an anonymous function
(print (lambda (x y) (+ x y)))

lexical scoping 이라면 이 코드는 (closure (t) (x y) (+ x y))을 두번 출력할 것입니다. dynamic scoping에선 (lambda ...)는 그 자체로 평가되지만, lexical scoping 에선 (closure ...)로 평가됩니다

이제 좀더 파고들어 봅시다. lexcial scoping 에서, 함수 A함수 B를 정의하고, 함수 B함수 C가 정의하고, 그 함수 Ca를 출력하면, a는 우선 C를 찾아보고 없으면, B를 등등을 찾아보게 됩니다.

dynamic scoping에서 my-func1란 함수를 가졌다고 가정해봅시다. 이 함수는 my-func2라는 함수를 호출하고, my-func2my-func3my-func3a를 출력합니다. 그리고 my-func2my-func3을 호출할때 내부적으로 a2로 설정한다고 해봅시다. dynamic scoping 에서 my-func1를 호출하면 어떤일이 발생할까요? 이는 2를 출력합니다. a1인 환경에서 my-func1를 호출하면 어떨까요? 여전히 1대신 2를 출력합니다. 다음 코드를 돌려봅시다.

(defun my-func1 ()
  (my-func2))
(defun my-func2 ()
  (let ((a 2))
    (my-func3)))
(defun my-func3 ()
  (print a))
(let ((a 1))
  (my-func1))

my-func1가 호출되고 my-func2가 호출되고 깊게 들어가는 동안, a1로 한 local binding 이 살아있습니다. my-func2a1로하여 이전 binding을 가리도록 또 다른 local binding 을 만들었습니다. 이 시점에서, 저희가 X라는 정소에 있다면 (let ((a 1)) (let ((a 2)) X ))에 있는 것과 같습니다. 이 지점에서 my-func3이 호출되어 2가 출력되게 됩니다.

dynamic scoping 에서 골치아프지만 여러분이 꼭 알아야만 할 것이 있습니다. 함수가 함수를 인자로 취하도록 만들길 원한다고 가정해 봅시다. 간단한 예제 함수가 준비되어있습니다.

(defun my-call (f n)
  (funcall f n))

(my-call #'1+ 5) ; => 6
(my-call #'oddp 5) ; => t

(dolist (i (list 1 2 3))
  (print
   (my-call (lambda (x) (* i x)) 5))) ; prints 5 10 15

지금까진 놀랄만한게 없습니다. 다음으로 넘어가 보도록 하겠습니다.

(dolist (n (list 1 2 3))
  (print
   (my-call (lambda (x) (* n x)) 5))) ; prints 25 25 25 in dynamic scoping.

무슨일이 발생한 것일까요? 왜 이러한 행동을 할까요? 문제는 (lambda (x) (* n x))에서 사용된 nmy-call의 인자의 이름중 하나와 같기 때문입니다. 인자 n5로 bind된 my-call내부에서, 익명함수 (lambda (x) (* n x))이 호출됩니다. lexical scoping 에선, 위 코드는 기대한대로 5 10 15을 출력합니다.

  • 발견한것 1 - dynamically scoped 함수를 다른 함수의 인자로 넘겨주는 것은, 나중에 발목을 잡을 수 있다!

하나 더 발견해봅시다. fg 함수를 취해, g를 먼저 적용하고 f를 적용시킨 것과 동일한 합성함수를 반환하는 함수를 정의해 봅시다.

;; in dynamic scoping
(defun my-compose (f g)
  (lambda (x)
    (funcall f (funcall g x))))

(funcall
 (my-compose (lambda (n) (+ n 3)) (lambda (n) (+ n 20)))
 100) ; results in error, Lisp error: (void-variable f)

에러는 f가 정의되지 않았다고 말해주고 있습니다. 왜 그럴까요? my-compose에서 함수가 합성되었지만, fg가 bind되지 않은 곳에서 호출되었습니다. 다시돌아와서, lexical scoping 에선, 위 코드는 기대한 대로 동작합니다.

  • 발견한것 2 - dynamically scoped 함수에서 반환된 함수를 이용하는 것은, 나중에 발목을 잡을 수 있다.

Emacs 24에서, defvar는 special variables라 불리는 것을 생성합니다. Special variables는, lexically scoped 함수 안이라 할지라도 dynamically하게 bind되는, dynamically scoped 변수(variables)입니다. case-fold-search는 special variable의 한 예입니다. 대소문자를 구분하는 함수 search-forward는 special variable case-fold-search의 값에 영향을 받습니다. (search-forward "hello")case-fold-searcht일때 HELLO를 찾지만, case-fold-searchnil일때는 그렇지 않습니다. lexically scoped el 파일에서 대소문자를 결정하기 위해 case-fold-search를 이용하되 추가 옵션을 지닌, my-search-forward 함수를 정의한다고 가정해봅시다. case-fold-search가 special variable이기에, 다음을 호출하면

(let ((case-fold-search t))
  (my-search-forward "hello"))

대소문자를 구분하지 않고 검색한다는 것을 확신할 수 있을 것입니다.

variable이 special인지 확인하기 위해, 함수 special-variable-p를 이용할 수 있습니다

(special-variable-p 'print-level) ; => t
(special-variable-p 'print-length) ; => t
(special-variable-p 'debug-on-error) ; => t
(special-variable-p 'debug-on-quit) ; => t

Special variables는 유용할 수 있습니다. reddit의 gsg가 말할길:

Dynamic scope는 명시적으로 인자를 넘기지 않고도 코드를 제한할 수 있습니다. 이는 보통 좋지 않으나, 몇몇 코드는 이로부터 덕을 봅니다.

kragensitaker가 말하길:

Thread-local variables, exception handlers, current locale, current clipping region, image transform들이 dynamically하게 scope하기에 좋은 예입니다 .

그럼 이제, lexical closures 로 할 수 있는 일을 살펴봅시다.

다음 코드를 lexical scoping 에서 돌려봅시다.

(let (c)
  (defun my-get-c ()
    c)
  (defun my-set-c (new-c)
    (setq c new-c))
  (defun my-add-to-c (x)
    (setq c (+ x c))))

세개의 함수를 이용하는 다음 코드를 돌려봅시다. lexical scoping 에서 돌린거나 그렇지 않은 곳에서 돌린거나 결과는 같은데, dynamically scoped 환경에서 호출된 lexically scoped 함수들은 여전히 lexically scoped 함수이기 때문입니다.

(my-set-c 10)
(my-add-to-c 5)
(print (my-get-c)) ; prints 15.
(my-add-to-c 1)
(print (my-get-c)) ; prints 16
(let ((c 0))
  (print c) ; prints 0
  (print (my-get-c))) ; prints 16.

my-get-c, my-set-c, my-add-to-c가 공유하는 c에 대한 binding은, private 변수처럼 행동하며 (let ((c 0)) ...)처럼 c에 대한 다른 binding에 대해 독립적입니다. 이는 세개의 defun form을 감싸는 let form에서 만들어진 c에 대한 binding이, 이 세개의 함수의 접근을 제외하고는 다 만료시키기 때문입니다.

이제 lexical closure 의 이용하여 C에서의 static 변수가 하는 일을 해봅시다.

(require 'cl) ; for incf
(eval
 '(let ((i 0))
    (defun my-counter ()
      (prog1
          i
        (incf i))))
 t)
(my-counter) ; => 0
(my-counter) ; => 1
(my-counter) ; => 2
(let ((i 10))
  (my-counter)) ; => 3
(my-counter) ; => 4

위 코드가 어떻게 동작하는지 어려워하시는 분을 위해, 여기 자세한 예제코드가 있습니다.

(eval
 '(let ((i1 0))
    (defun my-test ()
      (let ((i2 0))
        (prog1
            (list i1 i2)
          (incf i1)
          (incf i2)))))
 t)
(my-test) ; => (0 0)
(my-test) ; => (1 0)
(my-test) ; => (2 0)

my-test를 정의하고 3번 호출하였습니다. my-test속 (let ((i2 0)) ..) form은 my-test가 호출될때마다 실행되어 3번 호출되었습니다. 반면, (let ((i1 0)) ... ) form은 my-test가 정의될때 단 한번만 실행됩니다. 도움되셨길 바랍니다.

이제 lexical closure 인 함수를 반환하는 함수를 테스트 해봅시다.

(eval
 '(defun my-get-counter (start step)
    (let ((count start))
      (lambda ()
        (prog1
            count
          (setq count (+ count step)))))
    )
 t)

(setq my-get-even-numbers (my-get-counter 0 2)
      my-get-odd-numbers (my-get-counter 1 2))

(funcall my-get-even-numbers) ; => 0
(funcall my-get-even-numbers) ; => 2
(funcall my-get-even-numbers) ; => 4

(funcall my-get-odd-numbers) ; => 1
(funcall my-get-odd-numbers) ; => 3
(funcall my-get-odd-numbers) ; => 5

(funcall my-get-even-numbers) ; => 6
(funcall my-get-even-numbers) ; => 8

(setq my-get-even-numbers-2 (my-get-counter 0 2))
(funcall my-get-even-numbers-2) ; => 0
(funcall my-get-even-numbers-2) ; => 2
(funcall my-get-even-numbers-2) ; => 4

(funcall my-get-even-numbers) ; => 10
(funcall my-get-even-numbers) ; => 12
(funcall my-get-even-numbers) ; => 14

여러분들은 아마 my-get-even-numbers, my-get-odd-numbers, my-get-even-numbers-2하나의 count를 공유하는 대신 왜 자기만의 count를 가지고 있는지 혼란스러워 하실지도 모르겠습니다. 이들은 실제로 자신만의 count를 가지고 있습니다. 혼란스러우신 여러분을 위해, 다음 코드를 lexical scoping 에서 돌린다면 어떻게될까요?

(let ((count 0))
  (setq my-count
        (lambda ()
          (prog1
              count
            (setq count (1+ count))))))
(let ((count 0))
  (setq my-count-2
        (lambda ()
          (prog1
              count
            (setq count (1+ count))))))

my-countmy-count-2자기만의 count를 지니고있습니다. 각 let form은 각각 (setq .. (lambda ...)) forms을 감싸고 있습니다. 이는 실제 my-get-counter가 하는 일과 같습니다. (my-get-counter ..)이 실행될 때마다, (let ((count ..)) (lambda ..))이 다시 실행되며, count에 대한 분리된 새로운 binding을 만들어내 새로운 함수만이 접근할 수 있도록 만들어 줍니다. (my-get-counter ..)을 3번 실행시키면, (let ((count ..)) (lambda ..))은 3번 실행되어, count에 대한 3개의 binding과 3개의 반환 함수를 만들어냅니다.

Alice는 지금 lexically scope환경인 el 파일에서 새로운 Emacs Lisp 코드를 작성하고있습니다. Alice가 새로운 작성한 lexically scoped 코드와 다른이가 작성한 dynamically scoped 코드를 섞는(interact)다면, 어떤일이 벌어질까요? 멈추는건 아닐까요?

간단한 예제로 해봅시다.

(eval
 '(defun my-bah ())
 t)

(eval
 '(fset 'my-bah-2 (symbol-function 'my-bah))
 nil)

my-bah함수는 lexically scoped 환경에서 정의되었습니다. 따라서 이는 lexically scoped 함수가 되어야만 합니다. 그러면 my-bah-2는 어떨가요?

Alice : "my-bah-2dynamically scoped 환경에서 정의되었어. 따라서 이는 dynamically scoped 함수가 되어야만 해."

Bob : "my-bah-2의 함수 공간은 my-bah의 함수 공간(cell)을 복사했어. my-bah의 함수 공간은 lexically scoped 함수를 포함하고 있지. my-bah-2의 함수 공간에 있는 것은 lexically scoped 함수와 동일해"

Alice : "잠깐. 이 함수들은 아무것도 하지 않잖아. 먼가 좀 해보도록 만들어보자. 반환값으로 lexically scoped인지 알려주도록 만들어보자."

다음 코드는 lexically scoped환경에선 t를, 그렇지 않으면 nil을 반환합니다. 여기서 lexical-binding 값을 확인하는 것은 좋지 않은 생각입니다.

(let ((x nil)
      (f (let ((x t)) (lambda () x))))
  (funcall f))

Alice는 the my-bah & my-bah-2 코드를 수정했습니다.

(eval
 '(defun my-bah ()
    (let ((x nil)
          (f (let ((x t)) (lambda () x))))
      (funcall f)))
 t)

(eval
 '(fset 'my-bah-2 (symbol-function 'my-bah))
 nil)

my-bah-2lexically scoped 함수인지 확인해 봅시다.

(my-bah) ; => t
(my-bah-2) ; => t

그럼, Bob이 생각한게 맞은건가요? 이와 유사한, defun을 사용하지 않은 코드로 테스트해봅시다.

(eval
 '(setq my-nah
        (lambda ()
          (let ((x nil)
                (f (let ((x t)) (lambda () x))))
            (funcall f))))
 t)

(eval
 '(setq my-nah-2 my-nah)
 nil)

(funcall my-nah) ; => t
(funcall my-nah-2) ; => t

(setq abc (+ 1 1))를 돌리면, 덧셈을 기술하는 표현식 (+ 1 1)이 우선 평가되고, 평가결과 숫자 2는 변수 abc에 할당됩니다. 이처럼, (setq my-nah (lambda ...))을 돌리면, 익명함수를 기술하는 표현식 (lambda ...)이 먼저 평가됩니다. lexical scoping 에선, 평가결과는 lexically scoped 함수 값인 (closure ....)처럼 보입니다. 그리고 그 결과 (closure ....)가 변수 my-nah에 할당됩니다.

(setq abc (+ 1 1))을 돌린후 (setq abc-2 abc)을 돌리면, 표현식 (+ 1 1)에 대한 평가는 단 한번만 일어납니다. (setq abc-2 abc)(+ 1 1)를 또다시 평가하지 않고, 단지 이미 계산된 결과 2abc-2에 저장합니다. (setq abc-2 abc)에서 평가한 것은 symbol abc 자체이며, symbol abc를 평가하면 2입니다. 이처럼, my-nah & my-nah-2 예제코드에서 (lambda ...) 표현식의 평가는 단 한번만 일어나며, (setq my-nah-2 my-nah)를 돌릴때 결과 (closure ...)는 평가되지 않으며 단순히 my-nah-2에 저장됩니다. (setq my-nah-2 my-nah)dynamically scoped 환경에서 돌아갈 지라도, anonymous function 표현식에 대한 평가가 lexically scoped 환경에서 발생하기에, 변수 my-nah-2는 결국 lexically scoped 함수를 지니게 됩니다.

lexically scoped 함수가 만들어져 dynamically scoped 환경에 들어가게 되도, 함수는 여전히 lexically scoped 함수로 남아있습니다.

defun my-bah 예도 유사합니다. symbol my-bah의 함수 공간(cell)은 lexically scoped 함수를 담고있습니다. 다음 테스트를 살펴보겠습니다.

(print my-nah-2)
(print (symbol-function 'my-bah-2))

lexically scoped el 파일에서 defun을 가지고 있고, 내부에서 free variable에 해당하는 것을 확인하려면, dynamically scoped 파일에 있는 또 다른 이름의 함수대신, el 파일에서 그 주변을 살펴보기만 하면 됩니다.

이제 my-nah-2 & my-bah-2 예제를 이해했을 것입니다. my-get-counter를 다시 살펴보도록 하겠습니다. (defun my-get-counter ...)lexically scoped el 파일에 있는 동안, my-get-counter가 반환하는 함수들은 lexically scoped 입니다. 확인해봅시다.

(eval
 '(progn
    (setq my-get-even-numbers (my-get-counter 0 2))
    (print (funcall my-get-even-numbers))
    (print (funcall my-get-even-numbers))
    (print (funcall my-get-even-numbers)))
 nil)

이는 0 2 4를 출력합니다. 여기서 Alice가 다시 "my-get-even-numbers 함수는 dynamically scoped 환경에서 정의됬어. 그런대 왜 lexically scoped 함수처럼 행동하는 거야?" 라고 의문을 표할지도 모릅니다. 변수 my-get-even-numbersmy-nah-2와 마찬가지로 lexically scoped 함수를 지니고 있습니다. 이해가 어려우신 분들을 위해, 우선 my-get-sum을 살펴보도록 하겠습니다.

(defun my-get-sum (x y)
  (+ x y))

my-get-sum에 있는 덧셈을 기술하는 (+ x y)는 표현식이며, my-get-sum(+ x y)의 평가결과를 반환하지, 표현식 (+ x y)자체를 반환하지는 않습니다. (my-get-sum 1 2)을 돌려본다면, 표현식 (+ x y)그대로를 반환하지 않고, my-get-sum안에서 (+ x y)가 평가된 3을 반환합니다

my-get-counter로 돌아가봅시다. my-get-counter(lambda ...)은 익명 함수를 나타내는 표현식입니다. 이 표현식은 my-get-counter 내부에서 한번만 평가됩니다. 평가결과는 변수 my-get-even-numbers에 저장된 것을 반환받은 (closure ...)입니다. (lambda ...)의 평가는 단 한번만 이루어지며, 이는 lexically scoped 함수 my-get-counter에서 이루어집니다. lexically scoped 함수에서의 lambda form의 평가는 항상 (closure ...)가 됩니다. 이것이 바로 어떻게 my-get-even-numberslexically scoped 함수를 지닐수 있는지에 대한 이유입니다.

그건 그렇고, 어째서인지 무심코 lambda form의 평가를 막아버린다면, lexically scoped 함수는 dynamically scoped 함수를 생성하고 반환할 수 있습니다.

(eval
 '(defun my-return-dynamically-scoped-function ()
    (list 'lambda '() 'a)
    )
 t)

(eval
 '(defun my-return-dynamically-scoped-function ()
    '(lambda () a) ; quoted lambda
    )
 t)

왜 이렇게 했는지, 의도는 잘 모르겠지만, 어쨋건 가능합니다.

이제 my-call 예를 다시 살펴보도록 하겠습니다.

(eval
 '(defun my-call (f n)
    (funcall f n))
 nil)

(eval
 '(dolist (n (list 1 2 3))
    (print
     (my-call (lambda (x) (* n x)) 5)))
 t)

이는 5 10 15를 출력합니다. Alice가 다시 말하길 "함수 fdynamically scoped에서 정의되었잖아. 그런데 왜 lexically scoped 함수처럼 행동하는 거야?" my-call에 들어온 익명함수들은 lexically scoped 환경에서 정의되었기에, my-call에 들어온 후라도 lexically scoped 처럼 상태를 유지합니다. 그래도 이해가 어려우신 분들을 위해서. (lambda ...)가 평가되고 그 결과가 my-call에 들어갔습니다. my-call은 그 결과를 지역변수(local variable) f에 저장했습니다. 따라서 flexically scoped 함수를 참조하게 됩니다.

mapcar* 함수는 함수를 인자로 받고, dynamically scoped el 파일에서 정의되었다는 점에서 my-call와 비슷합니다. 다음 dynamic scoping 예제는 StackOverflow 답변에서 나온 것입니다.

(let ((cl-x 10))
  (mapcar* (lambda (elt) (* cl-x elt)) '(1 2 3)))

name cl-xmapcar*의 정의에서 argument name으로 사용됩니다. 따라서 dynamically scoped 환경에서 이 코드를 돌리면 깜짝 놀라게 될 것입니다(발견한것 1). 하지만 lexically scoped 환경에서 코드를 돌리면, 정상적으로 동작하는데 mapcar*에 들어온 lexically scoped 익명 함수는 여전히 lexically scoped 함수를 유지하기 때문입니다.

이 예제로 보아, lexically scoped 코드가 잘 어울리는것 같습니다. 이제 lexical scoping 을 즐기러 나가볼 시간입니다!