Lisp에서 Dynamic/Lexical Binding와 JavaScript의 var/let
다음의 간단한 코드를 읽어보자.
maker()()
의 결과는 'dynamic'
이다.
심지어, 맨 마지막에 있는 {..}
블록을 넘어가서도 x
의 값은 여전이
'dynamic'
이다.
자바스크립트에서 binding은 어딘가 이도 저도 아니게 심각하게 고장난
느낌이다. 이 글에서 이야기할 lexical binding이나 dynamic binding에도
속하지 않는 느낌이고, 이번에 새로 추가된 let
도 이해하기가 어려운게
아니라 그냥 제대로 망가져 있는거 같다.
그나마, let
키워드를 이용하면, lexical binding으로서나마 제대로
동작하기 시작한다.
그러면, lexical binding와 dynamic binding이 뭐길래 나는 이렇게 괴로워 하는가.
dynamic binding & lexical binding
커먼리습으로 예시 코드를 짜봤다:1
|
|
dynamic binding와 global variable
defvar
으로 global-scope에 만든 변수, *x*
을 되돌리는 함수를 만드는
함수, f-dynamic
이고, 이를 호출 하는 시점에 let
으로 이를 덮어
씌운다. 그리고 결과도 덮어 씌운 값으로 나온다.
이는 마치 다음이 괜찮은 코드라는 의미:
그런데, 커먼리습에서는 이게 된다.
심지어, 커먼리습의 대부분의 함수, 매크로는 이렇게 쓰는게 일반적인 idiom이다:
다른 언어였었다면:
*some-stream*
을print-sth
이 종속하므로, 2- 파라미터로 받거나, (default value이 지정되어 있는 optional parameter이라던가)
- 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
을 사용하지만, dynamic binding와는 다르게, f
을
호출해서 스택프레임이 달라지면 덮어 씌우지 못한다.
이해하기 단순, 간단하다.
closure와 lexical binding
사실, lexical scope만 제대로 있어도 여러모로 편안해진다.
- 블록의 단계별로 같은 이름인 변수의 덮어쓰기, 요즘 흔히 말하는 shadowing.
- 그리고 요즘 많이 알려진 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에도 추가할 수 있겠지)
|
|
조금 괄호가 복잡해보인다. 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 으로 지정해줘야 함.
위 코드는 dynamic binding으로 동작한다.
lexical binding을 쓰려면 다음과 같이 달라진다.
맨 첫 번째 특별한 주석으로 표기한 부분을 눈여겨 보라.
Clojure
Clojure은 기본은 lexical binding을 지원한다. (let
을 통해서)
그렇지만, ^:dynamic
metadata와 binding
으로 dynamic binding을
지정 가능하다:
http://clojure.github.io/clojure/clojure.core-api.html#clojure.core/binding
결론
커먼리습하자.
Footnotes
다른 Lisp이 아닌 언어로는 짜기가 어렵거나, 아예 가능하지도 않으니까. 그리고 커먼리습이 Lisp-dialects 중에서 이를 표현하기에 명확하다고 생각한다.
이렇게 dynamic binding인 변수는, 커먼리습의 관례는 *...*
와
같이 표기한다. 참고로 상수는 +...+
. –> Google Common Lisp Style
Guide
오히려 전역변수는 없는 쪽이 가깝다. 다른 변수로 상태를 표현하고
싶다면, defstruct
, defclass
으로 객체를 만드록 정리하겠지.