Chap #3December 03, 2011

Lisp 이해하기1

이번 장에서는 이맥스를 확장하고 이해하는데 반드시 필요한 기본적인 Lisp의 특성들에 대해 알아본다. 이맥스 Lisp의 개발환경(Inferior Emacs Lisp)을 시작으로, 문법(Form), 리스트(List)와 계산(Evaluate)의 개념들을 하나씩 이해해보자. 이번 장의 마지막에서는 기본적인 개념들을 바탕으로, Lisp이 어떻게 "코드를 데이터처럼" 다룰 수 있는지 이해해보도록 한다.

Lisp 개발환경

1장에서는 *scratch* 버퍼에서 표현식(expression)을 계산(evaluate)하는 방법을 알아보았다. 이맥스에서 다양한 방법으로 표현식(expression)을 계산(evaluate)할 수 있는데, 이번 절에서는 버퍼를 이동하지 않고 표현식을 계산하는 방법, IELM 모드에서 표현식을 계산하는 법을 알아본다.

스크래치 버퍼

우리가 이미 알아본 방법으로 *scratch* 버퍼의 표현식 끝에서 C-j을 입력하여 표현식을 계산할 수 있다.

그림-1. *scratch* 버퍼에서 계산(evaluate)하기

그림-1. *scratch* 버퍼에서 계산(evaluate)하기

미니 버퍼

두 번째 방법은 버퍼를 이동하지 않고 현재 버퍼의 환경 안에서 표현식을 계산하는 방법으로 M-:를 입력하면 미니버퍼에서 표현식을 계산할 수 있다.

그림-2. 미니 버퍼에서 표현식 계산(evaluate)하기

그림-2. 미니 버퍼에서 표현식 계산(evaluate)하기

IELM 모드

IELM(Inferior Emacs Lisp Mode)는 상호적(interactive)으로 표현식을 계산하는 방법으로, 스크립트형 언어들에서 제공하는 REPL(Read-Eval-Print Loop)과 같은 환경이다. M-x ielm을 입력하면 아래와 같은 화면이 보이는데, 현재 프롬프트에서 표현식을 입력하면 다음 라인에서 표현식을 계산하고 그 결과값을 출력해 준다.

그림-3. IELM 모드에서 표현식 계산하기

그림-3. IELM 모드에서 표현식 계산하기

독자들은 제시된 방법들 중 간편한 방법으로 앞으로 나오는 표현식을 자유롭게 계산해 보면 된다.

Lisp의 문법 (Form)

Lisp 프로그램은 표현식(expression)들로 이루어진다. 가장 간단한 형태의 표현식은 아톰(atom)이라고 불리는데, 정수과 문자열 등이 이에 해당한다. 정수와 문자열은 계산(evaluate)했을 때 입력한 그대로의 값을 갖는다.

그림-4. 정수와 문자열 계산하기

그림-4. 정수와 문자열 계산하기

ELISP> 123
123
ELISP> "hello world"
"hello world"

재미있는 형태의 표현식은 우리가 앞서 살펴본 괄호 안에 여러개의 표현식을 담고 있는 형태로 앞으로 리스트(List)라고 부른다.

ELISP> (message "hello world")
"hello world"

리스트 형태의 표현식이 계산될 때에는 각각의 원소(element)들이 좌에서 우로 계산이 되며, 첫 번째 원소는 함수 정의로 나머지 원소들은 함수 인자들로 계산된 후, 함수를 호출하게 된다. 또한 호출된 함수가 리턴하는 결과 값을 그 표현식의 값(value)이라고 부른다.

첫 원소를 함수로 나머지 원소를 함수의 인자들로 표현하는 리스트 형태(form)가 Lisp의 문법의 전부이다. (이러한 규칙의 예외들을 특수 형태(special form)라고 부르는데, 이들에 대해서는 다음 장에서 자세히 알아본다.) 이렇게 간단한 형태의 문법은 다양한 장점을 가지고 있는데, 대표적인 장점인 메타 프로그래밍(Meta Programming)에 대해서는 이번 장의 마지막에서 살펴볼 것이다.

표현식 계산하기 (Evaluate)

리스트 형태의 표현식들이 어떻게 계산(evaluate)되는지 아래의 예를 가지고 차근차근 알아보자.

ELISP> (message "hello: %d" (+ 1 2))
"hello: 3"
  1. 리스트 형태의 표현식이 주어졌으므로, 좌에서 우로 각각의 원소(element)를 계산한다.
  2. 첫 번째 원소는 message의 이름을 갖는 함수 정의로 계산된다.
  3. 두 번째 원소는 아톰(atom)으로 문자열 "hello: %d" 그대로의 값으로 계산된다.
  4. 세 번째 원소는 또 다른 리스트 형태의 표현식으로, (동일하게) 좌에서 우로 각각의 원소를 계산한다.
    1. 첫 번째 원소는 +의 이름을 갖는 함수 정의로 계산된다. (더하기)
    2. 두 번째 및 세 번째 원소는 아톰으로 1과 2로 계산되며,
    3. 더하기 (+) 함수를 1과 2의 인자로 호출 후 결과 값인 3이 리턴되어,
    4. 현재 표현식은 3이라는 값으로 계산이 되었다.
  5. message 함수를 위에서 계산된 "hello: %d"와 3을 인자로 호출 후 결과 값인 "hello: 3"이 리턴되어,
  6. 결과적으로 주어진 표현식이 "hello: 3"이라는 값으로 계산되었다.

알아차렸겠지만, 리스트 형태의 표현식은 다른 리스트 형태의 표현식을 재귀적으로 포함할 수 있으며, 표현식을 계산하는 과정 또한 재귀적으로 적용되고 있다.

심벌 (Symbol)

위의 예제에서 자신 그대로의 값으로 계산되지 않았던, message+를 Lisp에서는 심벌이라고 부른다. 이들은 쓰여진 위치에 따라 할당된 함수나 값으로 계산된다.

ELISP> fill-column
80
ELISP> (message "width %d" fill-column)
"width 80"

위에서 fill-column 심벌은 값(80)으로, message 심벌은 함수로 계산됨을 알 수 있다. 심벌은 리스트 형태의 표현식의 첫 번째 원소의 위치에 쓰였을때 함수로 계산되며, 그 외의 경우는 값으로 계산된다. 예를 들어 message 심벌은 값으로 계산하려고 하면 아래와 같이 값을 찾을 수 없다는 에러가 발생한다.

ELISP> message
*** Eval error ***  Symbol's value as variable is void: message

심벌의 앞에 ' (quote, 작은 따옴표)를 붙여 계산을 미룰수 있는데, 아래의 표현식을 계산해보자.

ELISP> 'message
message

즉 작은 따옴표(quote)를 message 심벌에 붙임으로써, 'message를 계산하면 message 심벌이 리턴되는 표현식을 만들 수 있게 되었다. 그러면 리스트 형태의 표현식에 '를 붙이면 어떻게 될까?

ELISP> '(message "width %d" fill-column)
(message "width %d" fill-column)

리스트 형태의 표현식에 '를 붙여 계산하면 리스트 그 자체가 리턴된다. 다시말해 계산하면 리스트 형태의 표현식이 값이 되는 표현식을 만든 것이다. '를 리스트 형태의 표현식에 적용한다는 것은 각각의 원소에 '를 적용하고 그 심벌들을 다시 리스트로 만드는 것이다.

ELISP> (list 'message '"width %d" 'fill-column)
(message "width %d" fill-column)

리스트 (List)

Lisp의 이름, LISt Processing이 의미하듯, Lisp은 리스트로 구성된 표현식을 계산하는 언어다. 다시 말해 리스트는 Lisp의 가장 가장 근본을 이루는 데이터 구조로, Lisp을 제대로 이해하기 위해서는 반드시 이해하고 넘어가야하는 개념이다. 이번 장에서 어떻게 리스트를 만들고 사용하는지 차근차근 알아보도록 한다.

먼저 리스트를 만들기 위해 필요한 함수 cons (constructor)를 살펴 보자. cons 함수는 car (address)과 cdr (decrement)로 지칭되는 두개의 인자를 받고, 두 인자를 묶어 하나의 cons 구조를 리턴한다.

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

(cons CAR CDR)

Create a new cons, give it CAR and CDR as components, and return it.

다음의 표현식을 계산해보자.

ELISP> (cons 1 2)
(1 . 2)

위의 cons 구조는 일반적으로 아래와 같이 도표화 하여 나타낼 수 있다.

           +---> 2 (cdr)
  cons     |
     +---+-|-+
     | o | o |
     +-|-+---+
       |
       +---> 1 (car)

좌/우를 하나의 cons 구조라고 칭하며, 각각의 박스는 해당하는 값(value)을 가리키게 된다. 또한 묶어진 (cons)의 값(value) 1과 2는 car과 cdr의 함수로 접근할 수 있다. car 함수는 cons 구조를 인자로 받아 첫 번째 값 1을 리턴하며, cdr 함수는 cons 구조를 인자를 받아 두 번째 값 2를 리턴한다.

ELISP> (car (cons 1 2))
1
ELISP> (cdr (cons 1 2))
2

조금더 복잡하게 cons 구조를 생성해 보자.

ELISP> (cons 1 (cons 2 (cons 3 4)))
(1 2 3 . 4)

위와 같은 cons 구조는 다음과 같은 다이어그램으로 나타낼 수 있다.

  cons          cons          cons
     +---+---+     +---+---+     +---+---+
     | o | o------>| o | o------>| o | o------> 4
     +-|-+---+     +-|-+---+     +-|-+---+
       |             |             |
       +---> 1       +---> 2       +---> 3

위의 cons 구조는 Lisp의 모든 데이터 구조의 기본이 되며, cons의 오른쪽 박스가 다른 cons 구조를 가리키게 되면서 여러 개의 cons를 묶을 수 있게 된다. 리스트 또한 cons의 구조를 활용하여 표현할 수 있는데, 리스트의 끝을 나타내기 위해 nil (null 또는 ground 라고도 불림)을 마지막 원소로 넣어 리스트를 표현한다.

즉 (cons 1 nil)은 1을 하나의 원소로 갖는 리스트이며, "(1)" 이라고 표현되고 아래와 같은 도표로 나타낸다.

  cons                                 
     +---+---+     
     | o | o------> nil
     +-|-+---+     
       |           
       +---> 1     

또한 1,2,3을 원소로 갖는 리스트는 아래와 같이 표현할 수 있다. 즉 리스트를 구성하는 하나의 cons 구조의 왼쪽 박스는 값을 가리키고, 오른쪽 박스는 다음 cons 구조를 가리키게 된다. 다음 cons 구조가 없는 리스트의 마지막 cons 구조는 nil을 가리키게 되며, 리스트의 마지막을 명시적으로 표시하게 된다.

  cons          cons          cons
     +---+---+     +---+---+     +---+---+
     | o | o------>| o | o------>| o | o------> nil
     +-|-+---+     +-|-+---+     +-|-+---+
       |             |             |
       +---> 1       +---> 2       +---> 3
ELISP> (cons 1 (cons 2 (cons 3 nil)))
(1 2 3)

그러면 비어 있는 (0개의 원소를 갖는) 리스트는 어떻게 표현할까?

ELISP> ()                               ; empty list
nil                                     ; nil == (), ground or nil/null

비어 있는 리스트는 () 또는 동일하게 (특별히) nil 심벌로 나타낸다. 자 그럼 하나의 원소를 갖는 리스트는 아래와 같이 만들 수 있다.

ELISP> (cons 1 ())                      ; one element
(1)                                     ; 1 -> nil
ELISP> (cons 1 nil)                     ; equally
(1)                                     ; 1 -> nil
ELISP> (cons 1 (cons 2 nil))            ; two elements
(1 2)                                   ; 1 -> 2 -> nil
ELISP> (cons 1 (cons 2 (cons 3 nil)))   ; three elements
(1 2 3)                                 ; 1 -> 2 -> 3 -> nil
ELISP> (list 1 2 3)                     ; three elements
(1 2 3)                                 ; 1 -> 2 -> 3 -> nil

물론 list 함수를 이용하면 더욱 간단하게 리스트를 만들 수 있다. 위의 예제처럼 여러 개의 cons를 사용하는 대신에 "(list 1 2 3)"를 이용해 동일한 리스트 구조를 만들 수 있다.

메타 프로그래밍 (Meta Programming)

이번 장에서 배운 개념들을 바탕으로 간단한 메타 프로그래밍의 개념을 이해하여 보자. 리스트 형태의 표현식에 '를 붙여 계산하면 원래의 리스트가 리턴됨을 보았다. Lisp의 eval 함수는 표현식을 받아 이를 계산하는 함수이다.

ELISP> (message "width %d" fill-column)
"width 80"

ELISP> (eval (message "width %d" fill-column))
"width 80"

그런데 eval에 표현식을 어떻게 인자로 넘길 수 있을까? eval은 특수 형태(special operator)가 아니어서 각각의 인자는 eval을 호출하기 전에 이미 계산되어 버린다. 위 예의 두 번째 표현식은 eval함수를 문자열 "width 80"을 인자로 호출한 것으로, 아톰인 문자열이 그대로의 값으로 계산되었다. 다음의 예를 보자.

ELISP> '(message "width %d" fill-column)
(message "width %d" fill-column)

ELISP> (eval '(message "width %d" fill-column))
"width 80"

ELISP> (eval (list 'message "width %d" 'fill-column))
"width 80"

우리가 앞서 이해한 '를 사용하면 표현식을 전달할 수 있지 않을까? 그렇다. '를 붙인 표현식을 eval 함수의 인자로 전달하면, 위의 예제의 마지막 표현식과 같이 심벌들의 리스트를 전달할 수 있다.

우리가 작성한 코드는 심벌들의 리스트임을 알았다. 즉 코드가 데이터의 형태를 띄고 있기 때문에 데이터를 원하는 형태로 주무르듯이, 코드 또한 원하는 형태로 수정하고 확장할 수 있게 되었다.

위의 코드를 한번 수정해 보자.

ELISP> (cdr (list 'message "width %d" 'fill-column))
("width %d" fill-column)

ELISP> (eval (cons 'message-box 
                   (cdr (list 'message "width %d" 'fill-column))))
"width 80"

위의 코드는 먼저 cdr을 이용해 첫 원소인 message 함수를 제거한 후 message 함수 대신 message-box 함수를 같은 인자들로 호출하는 과정을 보여준다. 리스트인 코드를 수정해 message 함수 대신 message-box 함수를 호출 가능하게 되었다.

조금 철학적인 이야기를 하자면, 우리가 하려고 하는 일은 "message-box 함수를 이용한 출력"이었다. 하지만 우리가 하고 있는 일은 "message-box 함수를 이용한 출력"을 하기위해 해당되는 코드를 짜는 일이 되었다. 흔히들 이러한 작업을 메타 프로그래밍이라고 부른다. 더 나아가 domain specific 언어를 디자인 한다라고 말한다. Lisp에서 이 두가지의 개념을 가능하게 했던 근간을 살펴보면 프로그래머가 표현하는 코드가 파싱해야 하는 문자열이 아니라, 리스트 데이터 였다는 사실이다. 코드가 리스트라는 구조있는 데이터로 표현됨으로써, 코드(리스트 데이터)를 프로그래머가 쉽게 수정할 수 있게 되었다.

사람들은 왜 php, javascript와 python의 eval() 함수를 피해야 할 패턴으로 분류하면서 Lisp의 이러한 특성들을 동경?하는 것일까? 한가지 Lisp이 다른 언어와 다른 점은 코드를 구조화된 데이터로 관리할 수 있다는 점인데, javascript와 python에서 eval()을 사용하기 위해서는 코드를 구조가 없는 문자열로 변환 후 전달해야 하기 때문이다. 즉 다른 언어의 eval()은 코드 문자열을 생성하는 과정에서의 외부의 작은 변화가 문자열의 전체 의미를 변화시킬 수 있기 때문에 항상 프로그래머의 의도대로 동작하지 않을 수 있다. 간단하게 말하면 구조화 된 코드를 구조가 없는 문자열로 변환하고 조작한 후 다시 코드로 변환하는 과정은 쉽게 오류를 범할 수 있을 뿐 아니라, 이해하기도 힘든 코드가 되기 때문이다.

정리

이번 장에서는 이맥스 Lisp의 문법(Form), 리스트(List)와 계산(Evaluate)의 개념들을 이해해 보았다. 다음 장에서는 Lisp의 특수 형태 (Special Form, Special Operator)를 이맥스의 테트리스 코드 속에서 찾고, 이해해 보려고 한다. M-x tetris를 실행해 볼까?

blog comments powered by Disqus