Chap #6January 30, 2012

모드(Mode)와 동적 바인딩(Dynamic Binding)

5장에서는 이맥스 내부의 키맵(Keymap) 구조와 기본적인 커서 이동법에 대해서 알아 보았다. 이번 장에서는 이맥스에서 파일을 여는과정을 Lisp의 구조적인 관점에서 살펴보고, Lisp의 동적 바인딩(dynamic binding)이 어떻게 이맥스의 주 모드(mode)를 구현하는데 사용되는 이해해보자.

파일 열기

6장 ~ 8장에서 같이 작성해볼 프로그램은 시저 암호화(caeser cipher) 알고리즘으로 암호화할 텍스트를 인자로 받아 암호화한 텍스트를 출력한다. 먼저 가장 많이 사용되는 컴파일형 언어인, C언어로 프로그래밍 해보자. 이맥스를 실행하고, 파일을 열기위해 C-x C-f: 파일 열기(find-file 함수)를 입력하자.

그림-1. 파일 열기

그림-1. 파일 열기

미니버퍼에 나타난 프롬프트가 보이는가? 미니버퍼에서 3가지 기능을 제공하는데, 각각을 나열하면 아래와 같다.

  • M-n: next-history-element: 다음 입력 히스로리
  • M-p: previous-history-element: 이전 입력 히스토리
  • TAB: minibuffer-complete: 자동완성

C-x C-f입력 후 TAB을 입력해 보면, 현재 폴더에 있는 파일들의 리스트를 *Completions* 버퍼에서 확인해 볼 수 있고, 부분적인 파일이름 입력 후 자동 완성됨을 확인해 볼 수 있다.

또한 이전에 입력했던 히스토리를 M-n(next)와 M-p(previous)의 키입력으로 찾아볼 수 있다. 이맥스에서 사용자의 입력은 completing-read를 기본적으로 사용하여 구현되어 있으며, 함수의 설명을 찾아보면 아래와 같다.

completing-read is a built-in function in `C source code'.

(completing-read PROMPT COLLECTION &optional PREDICATE REQUIRE-MATCH
INITIAL-INPUT HIST DEF INHERIT-INPUT-METHOD)
....

몇몇 인자들을 사용자 입장에서 살펴보자. 사용자는 INITIAL-INPUT의 초기값을 PROMPT와 함께 보게되고, TAB으로 자동완성을 하려고하면 위 함수는 COLLECTION에 있는 해당 인자를 찾아 프롬프트에 자동완성해준다. 또한 이전에 입력된 히스토리는 HIST에 기록되어 있어, 사용자가 M-nM-p를 입력하여 찾아볼 수 있다. find-file은 어떻게 completing-read를 사용하는지 살펴보자.

PROMT
"Find File" 프롬프트
INITIAL-INPUT
현재 디렉토리
COLLECTION
현재 디렉토리에 있는 파일들
HIST
file-name-history 변수로 사용자가 열어본 파일들을 기록

사용자의 입력은 항상 이와 같은 방법으로 이루어지지만, 파일을 읽는다거나 디렉토리를 읽는 것 같은 일반적인 일들은, read-file-nameread-directory-name과 같은 특화된 함수로 더욱 쉽게 호출이 가능하다.

자 버젼컨트롤 시스템을 구현한다고 하자. 만약 버전컨트롤에서 제외하고 싶은 특정 파일이나, 특정 패턴을 입력받고 싶다면 어떻게 할까? 이를 입력 받는 코드를 구현하기 위해서 completing-read를 사용하여 아래와 같은 코드를 작성할 수 있다.

(completing-read "Add to .gitignore >> "           ; prompt
                 '(".o", "*~", ".#*") nil nil      ; list of completions
                 (buffer-file-name)                ; initial input
                 'file-name-history)               ; reuse file-name-history

기본적으로 자주 입력되는 패턴들은 COLLECTION인자로, 현재 버퍼의 파일이름을 INITIAL-INPUT으로, 사용자가 열어본 파일 히스토리를 재사용하여 HIST인자로 전달할 수 있겠다.

이제 어떻게 사용자로부터 파일이름을 입력받았는지 이해했으니, 정말로 파일을 입력하고 생성해볼까? C-x C-f를 입력하고 앞으로 작성할 파일이름인 "enc.c"을 프롬프트에 입력해보자.

버퍼 상태바 (Mode Line)

새로운 파일을 생성했다면 아래와 같은 빈 버퍼를 볼 수 있다.

그림-2. 파일 열기

그림-2. 파일 열기

직관적인 (그래서 밋밋한) 메뉴와 툴바를 제외하면, 유일하게 호기심을 자극하는 것은 상태바 뿐이다. 상태바 위에 일단 마우스로 가져가 올려놓아 볼까? 상태바의 각각의 아이템들이 어떠한 상태를 나타내는지 툴팁을 통해서 볼 수있다. 또한 마우스의 어떠한 버튼을 클릭하면 어떠한 동작을 하는지 상세하게 나타내고 있다. (클릭해보고 싶지 않은가?)

 +- mode-line-mule-info (입력방법, U:Unicode)
 | +- mode-line-modified (버퍼 쓰기/읽기 가능 상태)
 | | +- mode-line-remote (원격/로컬 파일)
 | | | +- mode-line-frame-identification (버퍼이름)
 | | | |                               +- global-mode-string (주/부모드)
 v v v v                               v               
-----------------------------------------------------------------------
-U:--- enc.c           All L1          (C/l Abbrev)
=======================================================================
^ ^ ^                  ^    ^            
| | |                  |    +- mode-line-position (몇째 줄)
| | |                  +- mode-line-buffer-identification (화면 위치)
| | +- mode-line-modified (버퍼 수정 상태)
| +- end of line style (unix)
+- full memory

엄청 복잡해 보이지만, 입력방식, 버퍼의 상태, 커서의 위치, 주모드/부모드 등을 나타내고 있다. 이맥스에서 이러한 상태바를 구성하는 방식은 mode-line-format: 상태바 형식 변수를 통해서 정의하고 있고, 단순한 리스트의 형태를 하고 있다. 현재 상태바는 어떠한 값을 가지고 있는지 C-h v를 통해서 찾아 보자.

mode-line-format is a variable defined in `C source code'.
Its value is shown below.

  Automatically becomes buffer-local when set in any fashion.
  This variable is potentially risky when used as a file local variable.

Documentation:
Template for displaying mode line for current buffer.
...
For a symbol,  ...
For a list of the form `(:eval FORM)', ...
For a list of the form `(:propertize ELT PROPS...)', ...
A string is printed verbatim in the mode line except for %-constructs:
  ...
  %b -- print buffer name.      %f -- print visited file name.
  %F -- print frame name.
  ...

아마도 긴긴 문서에 정신을 못차렸을 것이다. (필자도 마찬가지이니 기죽지 말자.) mode-line-format은 우리가 무엇을 나타내고자 하는지, 리스트 형태로 정의해 주기만 하면된다. 문서에서는 리스트 안의 원소가 어떤 타입이냐에 따라 어떻게 해석할지, 타입에 따라 하나하나 나열해 놓은 것이다. (쉽게 상상하건데) 만약 Lisp의 심볼이 원소이면 심볼을 해석(eval)해서 상태바에 포함시킬 것이고, :eval의 심볼(단순한 ":eval" 이름의 심볼)로 시작하는 리스트라면 :eval을 제외한 나머지 원소들을 해석해서 출력할 것이다. 물론 편의를 위해 자주 쓰이는 기능들은 "%b"(버퍼이름)와 같은 문자열로 정의되어 있으니, 아래에 정의된 문자열을 설명해 놓았다.

간단하게 한번 실험해 볼까? 버퍼이름을 나타내는 정의된 문자열 "%b"와 현재 버퍼의 파일이름을 담고 있는 buffer-file-name을 리스트에 넣어 정의해 보자. ("enc.c" 버퍼에서 M-: 실행 후 표현식을 입력한다.)

(setq mode-line-format (list "%b" " => " 'buffer-file-name))

상태바가 아래와 같이 변경되었는가?

-----------------------------------------------------------------------
enc.c => /tmp/enc.c
=======================================================================

다음은 복잡하게 정의된 (하지만 일반적으로 쓰이는) 하나의 원소인데, 우리가 마우스를 상태바에 가져가면 나타나는 툴팁(help-echo)과, 색깔(face)을 같이 정의 하고 있다.

(setq mode-line-format
  (list
    ;; the buffer name; the file name as a tool tip
    '(:eval (propertize "%b " 'face 'font-lock-keyword-face
        'help-echo (buffer-file-name))))
   ....

이맥스가 어떻게 글자를 출력하고, 색깔을 입히는지 궁금해졌는가? 이맥스에서 글자들을 어떻게 정형화 해서 특성을 정의하고, 출력하는지 다음장에서 차근차근 알아볼 것다.

버퍼 로컬 변수

"enc.c" 버퍼의 모드라인이 원하는데로 변경되었는가? 그렇다면 다른 버퍼의 상태바는 어떠한가? *Scratch* 버퍼로 이동해보자. 이전 상태바 모습 그대로 출력되고 있는가, 아니면 우리가 변경한 상태바 문자열로 변경되었는가? 신기하게도 우리가 수정한 버퍼 이외에는 모두 이전 상태바를 여전히 가지고 있다.

mode-line-format 변수에 관한 문서를 다시 살펴 보자.

mode-line-format is a variable defined in `C source code'.
Its value is shown below.

  Automatically becomes buffer-local when set in any fashion.
  This variable is potentially risky when used as a file local variable.
...

만약에 어떠한 방법으로든 mode-line-format의 값을 변경하면 "buffer-local"이 된다고 한다! 일반적인 프로그래밍 언어에서 전역 변수, 지역 변수의 개념이 있다. 이맥스 Lisp에는 하나의 재미있는 변수의 범위가 하나더 있는데 이것이 "버퍼 로컬"이다. 즉, 각각의 버퍼는 자신만의 독립적인 mode-line-format의 값을 갖고 있다.

(참고) 파일안에 (일반적으로 주석으로) 그 파일이 이맥스에서 읽혔을때 어떠한 변수값을 이맥스 버퍼에서 갖도록 지정할 수 있다. 예를 들면, 커널 드라이버 소스 파일에 아래와 같은 주석을 흔히 볼 수 있다. (주로 파일 끝에 적혀있다.)

$ tail -6 linux-git/drivers/net/ethernet/i825xx/eexpress.c    
/*
 * Local Variables:
 *  c-file-style: "linux"
 *  tab-width: 8
 * End:
 */

즉, "eexpress.c" 파일을 열면, tab-width의 값을 8로 사용하도록 지정할 수 있다. 이렇게 파일 안에 현재 파일에 해당하는 Lisp 변수들의 값을, 파일 로컬 (file local) 이라고 부른다.

환경(environment)과 동적 바인딩(dynamic binding)

이전 장에서 설명했듯이 환경은 심볼(키)과 이에 해당하는 값이 기록되는 메모리 같은 공간이다. (다른 언어에서 제공하는 map, directory, hashtable 이라는 구조체라고 생각해도 될 것 같다.) 전역 변수 A가 있는데, 지역 변수 (같은 이름의) A를 선언했다면, 변수 A는 어떠한 값을 가질까? 물론 변수 A가 어디에서 쓰이는지에 따라 다른 값을 갖게 될 것이다. 그런데 환경은 단순한 심볼(이름/키)->값의 구조체인데 이를 어떻게 구조화 할까? 대부분의 언어에서 이러한 환경을 아래와 같이 나타낸다.

+------------------+ (*)
| local (func) env |         A: "local value"
+---------|--------+
+---------V--------+ (*)
| local (func) env |         
+---------|--------+
+---------v--------+ (1)
|    global env    |         A: "global value"
+------------------+

함수를 하나 호출할 때마다 하나의 환경이 생성되고, 함수에서 변수의 이름에 해당하는 값을 찾기위해서 가장 가까이 있는 (위의 지역 범위) 환경에서부터 변수이름에 대항하는 값을 찾아갈 것이다. 만약 환경에서 찾지 못하면? 컴파일 에러나 인터프리터 에러를 출력할 것이다.

편의상 이해를 돕기위해서 함수가 하나의 환경을 만든다고 했지만 (대부분의 언어는 그렇게 디자인 되어 있다.) 가장 작은 단위는 아니다. C언어를 예로 들면 하나의 블록(block) 단위로 환경이 정의된다고 생각하면 된다. (물론, 컴파일 단계에서 정확히 변수의 값을 결정하는 메타데이터로 사용될 것이다.)

다시, 언어는 선언된 변수의 이름이 의미있는 범위를 정하고 있는데, 만약 이 범위에 안에서 모두 값으로 결정되어지지 않으면 어떻게 할까? 이렇게 자기 범위안에서 값을 찾지못하는 변수들을 자유 변수(free variable)이라고 부르는데, 해당 값을 찾는 방법(resolve 또는 bind)에 따라 정적 바인딩 (static/lexical binding), 동적 바인딩 (dynamic binding) 으로 일반적으로 특징지어진다. (물론, 상상하기에 따라 무한한 방법으로 언어를 디자인 할 수 있지만, 위에 나열된 것들은 가장 직관적인 방법으로 분류되어 흔히 사용되는 개념들이다.)

정적 바인딩(static binding)은 소스코드가 주어지면, 모든 변수(이름)의 값이 무엇인지 결정지어진다. 이러한 이유로 lexical binding 이라고도 부르는데, 가장 많이 사용되고, 가장 쉽게 이해할 수 있는 구조이다.

하지만 Lisp은 동적 바인딩(dynamic binding)을 사용한다. 동적 바인딩은 함수 안의 자유 변수들의 값을 결정하는데, 현재 실행되고 있는 상태(context)가 영향을 준다. 즉 소스코드를 실행하는 과정에서 변수의 값을 결정짓게 된다. 이러한 특성이 이맥스를 확장성있게 만드는 큰 이유중에 하나이다.

자자 코드를 살펴보자.

;; in general
(let ((tab-width 4))
     (c-mode))

먼저 Lisp에서는 let를 통해서 새로운 변수이름과 값을 정의할 수 있다. (변수이름과 변수 값을 갖는 환경이 생성된다.) 만약 c-mode의 함수에서 (자유변수) tab-width의 변수이름을 사용한다면 어떠한 값을 갖도록 계산(evaluate)될까? Lisp의 동적 바인딩에 의해서 4의 값을 사용하게 될 것이다.

;; for linux kernel
(let ((tab-width 8))
     (c-mode))

필자는 탭의 간격을 대부분의 경우 4로 사용하지만, 커널 소스코드를 수정할 때는 탭 간격은 8, 스페이스 대신 탭을 사용한다. 만약 사용자가 두개의 소스파일 (두개의 버퍼), 하나는 일반적인 소스코드, 하나는 커널 소스코드를 사용한다면 c-mode는 어떻게 구현되어야 할까?

만약 정적 바인딩을 사용한다면, 아니 Lisp이 아닌 언어를 사용한다면, c-mode의 개발자는 고려해야 할 사항이 너무 많아진다. 물론! 구현이 불가능 한것은 아니다. 같이 상상해볼까? c-mode를 C++언어로 구현했다면 같은 코드가 여러 버퍼에 독립적으로 사용되기 위해서 아래와 같이 정의되야 할 것이다.

void c_mode(map<string,int> opts) {
    int tab_width = opts["tab_width"];
    ...
}

c-mode 함수에 버퍼마다 다르게 쓰여지는 탭의 간격을 "tab-width"의 키와 해당하는 값을 map으로 전달하고, 버퍼에 특징적인 일을 할 때마다 map에서 "tab-width"에 해당하는 값을 찾아 쓰면 되지 않는가? 우리는 정확히 Lisp의 동적 바인딩을 모든 함수에서 중복하게 구현하고 있는 자신을 발견할 수 있다. 많은 사람들이 Lisp의 기능/특성을 다른 언어에서 활용하는 방법은, Lisp 인터프리터를 구현하고, Lisp으로 프로그래밍 하는 것이 가장 빠른 방법이라고들 농담삼아 이야기 한다.

(defun c-mode () 
    ;; just use tab-width!
)

자 정리하면, 이맥스가 사용하는 환경은 아래와 같다.

+------------------+ (*)
| local (func) env | ...
+---------|--------+
+---------V--------+ (*)
| local (func) env | ...
+---------|--------+
+---------v-------------+ (+)
|    buffer-local env   |  ....
+---------|-------------+
+---------v---------------------------------------+ (1)
|                    global env                   |
+-------------------------------------------------+

버퍼의 특징적인 환경이, 버퍼의 주 모드 초기화 함수를 호출하기 전에 생성되며, 버퍼의 자유 변수들은 모두 현재 함수가 호출되고 있는 버퍼 안에서 순차적으로 값을 찾게된다. Lisp으로 주 모드를 구현하면, 마치 하나의 버퍼만 존재하듯 구현하고, Lisp의 동적 바인딩으로 많은 버퍼들에 독립적으로 모드 함수를 적용해서 사용할 수 있게된다.

정리

이번 장에서는 이맥스에서 파일을 수정하기 위해, 버퍼에 열어오는 과정을 Lisp의 구조적인 관점에서 간략하게 살펴보았다. 다음 장에서는 생성한 버퍼에 직접 프로그래밍 하는 과정에서 사용하는 특성들을 살펴보고, 이맥스로 프로그래밍하는 작업 싸이클은 어떠한지 살펴볼 것이다. 같이 시저 암호화 루틴을 여러가지 특징적인 언어들로 (컴파일형, 인터프리터형 등등) 코딩! 해보도록 하자.

blog comments powered by Disqus