Lisp에서 Dynamic/Lexical Binding와 JavaScript의 var/let

Posted on Dec 30, 2019

다음의 간단한 코드를 읽어보자.

var x = "lexical";

function maker() {
  return function() {
    return x;
  };
}

{
  var x = "dynamic";
  assert(maker()() == "???");
}

maker()() 의 결과는 'dynamic' 이다.

심지어, 맨 마지막에 있는 {..} 블록을 넘어가서도 x 의 값은 여전이 'dynamic' 이다.

자바스크립트에서 binding은 어딘가 이도 저도 아니게 심각하게 고장난 느낌이다. 이 글에서 이야기할 lexical binding이나 dynamic binding에도 속하지 않는 느낌이고, 이번에 새로 추가된 let 도 이해하기가 어려운게 아니라 그냥 제대로 망가져 있는거 같다.

그나마, let 키워드를 이용하면, lexical binding으로서나마 제대로 동작하기 시작한다.

const assert = require("assert");

let x = "lexical";

function maker() {
  return function() {
    return x;
  };
}

{
  let x = "dynamic";
  assert(maker()() == "lexical");
  assert(x == "dynamic");
}

assert(x == "lexical");

그러면, lexical binding와 dynamic binding이 뭐길래 나는 이렇게 괴로워 하는가.

dynamic binding & lexical binding

커먼리습으로 예시 코드를 짜봤다:1

  ;; every `defvar', `defparameter' is dynamic binding.
  (defvar *x* nil)


  (defun f-dynamic ()
    *x*)

  (let ((y 'lexical))
    (defun f-lexical ()
      y))

  (defun run ()
    (let ((*x* 'dynamic)
          (y 'dynamic))
      (declare (ignore y)) ; silence the: "style-warning: The variable Y
                                          ; is defined but never used."
      (let ((l (multiple-value-list (values (f-dynamic)
                                            (f-lexical)))))
        (pprint l)
        (assert (equal l '(dynamic lexical))))))

  (run) ; 실행

dynamic binding와 global variable

defvar 으로 global-scope에 만든 변수, *x* 을 되돌리는 함수를 만드는 함수, f-dynamic 이고, 이를 호출 하는 시점에 let 으로 이를 덮어 씌운다. 그리고 결과도 덮어 씌운 값으로 나온다.

이는 마치 다음이 괜찮은 코드라는 의미:

  x = 'no-dynamic'

  def f():
      return x

  def run():
      x = 'dynamic!'
      assert f() == 'dynamic!' # 실패한다. 
      # 여전히 f()안에서는, x == 'no-dynamic'이다.
  
  if __name__ == '__main__':
      run()

그런데, 커먼리습에서는 이게 된다.

심지어, 커먼리습의 대부분의 함수, 매크로는 이렇게 쓰는게 일반적인 idiom이다:

  (defvar *some-stream* *standard-output*)

  (defun print-sth (x)
    (format *some-stream* "~a~%" x))

  (print-sth 42)

  (let ((*some-stream* *standard-error*))
    (print-sth :error!!!!))

다른 언어였었다면:

  1. *some-stream*print-sth 이 종속하므로, 2
  2. 파라미터로 받거나, (default value이 지정되어 있는 optional parameter이라던가)
  3. builder pattern 같은 것을 사용해서 스트림을 지정하고, 그에 따라 동작하는 메서드로 print-sth 을 정리해야 했겠지

위에 말한 방법들 모두 커먼리습에서 쉽게 가능하고 많이 사용하는 방법이지만, 그래도 이런 방식으로 context 객체나 어떤 모듈에서 널리 공유하는 상태를 적용할 때 이렇게 dynamic binding 을 이용한다.

그리고 이렇게 적용하는 것도 defvar, defparameter, let 으로 간단하고 일상적이다.

전역변수와 같은 모양이지만, 전역변수랑 동일하게 생각하기는 조금 다르다. 언제나 문맥에 따라서 파라미터로 사용하기 위해서 있는 것이고, 또 그렇게 쓰도록 권장하니까.3

JS에서 var, let 모두 이렇게 만들기는 잘 모르겠다. 쉽지는 않을거 같다.

lexical binding

위 예제 코드에서 f-lexical 은 그 함수가 선언되는 시점, defun f-lexical 시점의 y 을 언제나 갖고 있고, dynamic binding와는 다르게, let 을 통해 변경할 수 없다.

또, 어떤 변수가 lexically하게 가장 가까운 scope에서 선언된 내용을 사용한다.

  (let ((x 'f))
    (defun f ()
      (pprint x)))

  (defun run ()
    (let ((x 'run-1))
      (let ((x 'run-2))
        (pprint x)
        (f))))

  #|
  CL-USER> (run)
  RUN-2
  F
  |#

같은 함수, 호출스택 안에서는 단순하게 가장 가까운 let 의 선언으로 덮어씌운 x 을 사용하지만, dynamic binding와는 다르게, f 을 호출해서 스택프레임이 달라지면 덮어 씌우지 못한다.

이해하기 단순, 간단하다.

closure와 lexical binding

사실, lexical scope만 제대로 있어도 여러모로 편안해진다.

  1. 블록의 단계별로 같은 이름인 변수의 덮어쓰기, 요즘 흔히 말하는 shadowing.
  2. 그리고 요즘 많이 알려진 closure으로 감싸는 환경environment, 그 변수를 감싸서 갖고 있는 것도, lexical closure 이다.

JS은 closure을 지원하기는 했지만, let 이전에는 제대로 lexical binding을 지원했다고 할 수는 없다. (맨 처음 섹션에서 보였듯이)

추가: Emacs-Lisp, Scheme, Clojure에서 lexical/dynamic bindings

오늘 포스팅은 딱히 엄청난 결론을 이끌어내지는 못해서 그냥 Lisp계열 언어들의 예제들을 나열이나 해보려고 한다.

Scheme

Scheme에서는 lexical binding만 지원한다.

하지만, dynamic binding은 SRFI-39 으로 라이브러리/함수로서 추가적으로 지원한다.

위 SRFI-39에서도 보이듯이, closure와 macro을 이용해서 스킴 컴파일러/인터프리터를 확장하지 않고도 추가할 수 있다. (그리고 어쩌면 이렇게 유사한 방법으로 JS에도 추가할 수 있겠지)

  (use-modules ((rnrs) :version (6)))

  (define var-dynamic (make-parameter 'dynamic))

  (define (lexical-maker)
    (let ((var-lexical 'lexical-1))
      (lambda () var-lexical)))

  (define (dynamic-maker)
    (lambda () (var-dynamic))) ; `var-dynamic' needs to be evaluated as
                               ; a function to get its' value.

  (define (run)
    (let ((var-lexical 'lexical-no!))
      (assert (eq? ((lexical-maker))
                   'lexical-1)))
    (parameterize ((var-dynamic 'dynamic-yes)) ; not just `let', it's
                                               ; `parameterize'.
      (assert (eq? ((dynamic-maker))
                   'dynamic-yes))))

조금 괄호가 복잡해보인다. Scheme의 특성으로, lambda-function을 값으로 되돌리고, 또 그걸 그대로 (...) 으로 감싸서 funcall 처럼 바로 평가해 버리니까 2중 괄호로 감싼게 좀 보인다.

make-parameter 으로 만든 dynamic binding은 사실 언어 차원에서 지원하는 바인딩이 아니고, 그냥 box-container의 일종일테고, parameterize 매크로를 사용해서 마치 커먼리습에서 let 을 이용해 값을 binding한다. ..그리고 또 괄호로 parameter object을 감싸서 함수로서 평가하여, bind된 값을 참조해낸다. (이렇게 괄호가 또 생긴다.)

GNU Guile 에서 테스트했다. Racket등에서는 use-modules 이 동일하게 동작할지 모르겠다. 아마 이 라인 대신에 #lang racket 정도로 대체하면 잘 동작할거 같다.

Emacs-Lisp

Emacs-Lisp은 원래는 dynamic binding만 지원했엇다.

그리고 지금은 lexical binding을 지원하기 위해서는 사용하려는 소스코드의 file local variable 으로 지정해줘야 함.

  (assert (eq 'dynamic
              (let ((x 'lexical))
                (let ((f (lambda () x)))
                  (let ((x 'dynamic))
                    (funcall f))))))

위 코드는 dynamic binding으로 동작한다.

lexical binding을 쓰려면 다음과 같이 달라진다.

  ;;; -*- lexical-binding: t; -*-
  (assert (eq 'lexical
              (let ((x 'lexical))
                (let ((f (lambda () x)))
                  (let ((x 'dynamic))
                    (funcall f))))))

맨 첫 번째 특별한 주석으로 표기한 부분을 눈여겨 보라.

Clojure

Clojure은 기본은 lexical binding을 지원한다. (let 을 통해서)

그렇지만, ^:dynamic metadata와 binding 으로 dynamic binding을 지정 가능하다:

http://clojure.github.io/clojure/clojure.core-api.html#clojure.core/binding

user=> (def ^:dynamic x 1)
user=> (def ^:dynamic y 1)
user=> (+ x y)
2

user=> (binding [x 2 y 3]
         (+ x y))
5

user=> (+ x y)
2

결론

커먼리습하자.

Footnotes


1

다른 Lisp이 아닌 언어로는 짜기가 어렵거나, 아예 가능하지도 않으니까. 그리고 커먼리습이 Lisp-dialects 중에서 이를 표현하기에 명확하다고 생각한다.

2

이렇게 dynamic binding인 변수는, 커먼리습의 관례는 *...* 와 같이 표기한다. 참고로 상수는 +...+. –> Google Common Lisp Style Guide

3

오히려 전역변수는 없는 쪽이 가깝다. 다른 변수로 상태를 표현하고 싶다면, defstruct, defclass 으로 객체를 만드록 정리하겠지.