2008년 9월 2일 화요일

여러가지 언어를 배운다는 것 - Why Lisp?

'실용주의 프로그래머'라는 책을 보면 한 달에 하나 정도 새로운 언어를 살펴보는 것이 도움이 된다는 얘기가 나온다. 어떻게 생각하는가? 새로운 언어를 배운다는 것은 노력은 잔뜩 들어가는 데 비해 얻는 것은 별로 없는 행동이 아닐까? 한 언어에서는 함수 정의를 위해 def를 쓰고 다른 언어에서는 defun을 쓴다는 걸 살펴보는 게 무슨 의미가 있을까?

언어를 배움으로써 무언가를 얻고 싶다면, 표기법의 차이가 아니라 개념의 차이를 살피려고 노력해야 한다. 언어를 익혀보면, 실제로 표기법의 차이를 익히는데 드는 시간은 그리 크지 않다. 대부분의 시간은 전에 알지 못하던 새로운 개념을 이해하는데 들기 마련이다. 언어는 사고를 지배한다. 극단적인 예이지만, 셋 이상의 수를 가리키는 말이 없는 언어를 쓰는 사람들은 수에 대한 개념이 극히 제한될 수 밖에 없다.

어떤 언어에는 있고 다른 언어에는 없는, 정말로 고유한 개념 같은 것이 있을까? 글쎄, 예를 들어 보자. Lisp이 다른 언어에 비해 가진 고유한 특징은 데이터와 코드의 형태가 같다는 것이다. Lisp의 자료구조 중 하나인 리스트는 코드를 표현하는 데도 쓰인다. 즉, 다음의 두 표현은 모두 리스트(데이터)이면서 코드이다.

(1 2 3 4)

(defun hello () "hello")

데이터와 코드의 형태가 같다는 것은 언어를 만드는 것과 프로그램을 짜는 것 사이의 경계가 없다는 것을 의미한다. 둘 다 기본 언어 위에 추상(abstraction)을 쌓아 올려서 우리가 표현하고자 하는 것을 쉽게 표현할 수 있게 만드는 것일 뿐이다. 예를 하나 들어 보겠다.

Erlang에서는 -compile(export_all). 옵션을 파일 위쪽에 선언하면 모듈 안의 모든 함수들이 다른 모듈에서 사용할 수 있도록 export 된다. Lisp에서는 어떤 패키지 안의 함수들을 export 하려면 다음과 같이 선언해 주어야 한다.

(export '(function1 function2 function3 ...))

Lisp에서 한 번의 선언을 통해 패키지 안의 모든 함수를 export 하는 with-export-all이라는 operator를 만들고 싶었다고 하자. with-export-all이 해야 할 일은 다음과 같다.

1. 코드 중에 defun(함수를 선언하는 symbol)으로 시작하는 코드가 있는지 살펴보고 있다면 함수 이름을 리스트에 저장한다.
2. 리스트에 저장된 함수 이름을 export 한다.

with-export-all을 사용하면 다음 예와 같이 확장(expansion)되어야 할 것이다.

(with-export-all
(defun hello ()
"hello")
(defvar *hey*)
(defun bye ()
"bye")))

=>

(PROGN
(EXPORT '(HELLO BYE))
(DEFUN HELLO () "hello")
(DEFVAR *HEY*)
(DEFUN BYE () "bye"))

위와 같이 동작하는 with-export-all의 코드는 다음과 같다.

(defmacro with-export-all (&body body)
(let ((function-list nil))
(loop for expression in body do
(if (equalp 'defun (first expression))
(push (second expression) function-list)))
(setf function-list (reverse function-list))
`(progn
(export ',function-list)
,@body)))

with-export-all은 리스트(코드)를 받아서 리스트의 각 요소가 함수를 선언하는 코드(리스트)인지 살펴보고 맞을 경우 함수 이름을 빼내서 리스트를 만든다. 그 뒤에 함수들을 export하는 부분을 추가한 코드(리스트)를 리턴한다. with-export-all이 하는 일은 코드를 받아서 살펴보고 조작한 코드를 리턴하는 것이다. 동시에 그것은 그저 리스트(데이터, 배열이나 벡터라고 봐도 무방할 수 있는)를 받아서 조작한 리스트를 리턴하는 것일 뿐이다.

다른 언어에서 비슷한 일을 하려면 코드를 받는 메써드를 만들어야 할 것이다. 하지만 다른 언어에서 코드는 데이터가 아니다. 따라서 코드를 넘기려면 데이터의 형식인 스트링으로 넘기던지 해야 할 터이지만, 위와 같은 예에서 파일의 코드 전체를 export_all이라는 메써드에 스트링으로 넘겨야 한다면 정상적인 코딩은 불가능해질 것이다. 코드 전체가 한 색깔로, indentation도 없이 표시되는 것을 보게 될 테니 말이다.

C의 매크로는 코드를 받아 코드를 리턴할 수 있다. 하지만 Lisp에서 데이터와 코드의 형태가 같다는 것은 데이터를 다루는 수준의 섬세함으로 코드를 조작할 수 있다는 말이 된다. C에서 위와 같이 코드를 살펴보고 복잡한 작업을 하는 매크로를 만드는 것드는 것은 어려운 일이다. 더구나 C의 매크로에는 결정적인 단점이 하나 있는데 매크로를 정의하는 데 사용되는 symbol의 name confliction(흔히 variable capture라고 일컬어지는 문제)을 피할 방법이 없다는 것이다.

(AOP를 사용하는 것은 위와 같은 operator를 사용하는 것과 다른데, 어떤 오퍼레이터가 적용되는지가 코드에 보이지 않기 때문이다. 예를 들어, 매크로를 사용하면 with-export-all-function, with-export-all-macro, with-export-all 등의 operator들을 만들어 상황에 따라 코드의 일부만 적절한 operator로 감싸는 것도 가능하다. 하지만 AOP를 사용해서 포인트컷을 정규식으로 표현하게 되면 그렇게 부분적이고 세밀한 적용은 불가능하거니와, 적용 여부가 명시적으로 눈에 안 보인다는 단점을 지닌다)

데이터와 코드의 형태가 같다는 것은 아직까지는 Lisp만의 고유한 특성이라고 할 수 있다. Lisp이 가진 가비지 콜렉션이나 interactive 프로그래밍 환경 등의 특성들을 다른 언어들이 가져가 도입했지만 아직까지도 데이터와 코드의 형태가 같다는 특성만은 취하질 못하고 있다. 왜일까? 이유는 나도 잘 모르겠다.

Lisp의 최고 장점으로 꼽히는 매크로의 힘과 우아함도 사실상 코드와 데이터의 형태가 같다는 데서 나오는 것이라고 할 수 있다(추가적으로 모든 operator가 리스트의 첫번째에 온다는 것이 언어가 가진 문법의 전부라는 것도 우아함을 만들어내는 요소라고 할 수 있다. 흔히 'Lisp에는 문법이 없다'라고 말해지기도 한다). Lisp의 매크로는 언어의 표현력에 가해지는 제약을 없앰으로써 프로그래머에게 무한한 자유를 부여한다.

요지는 언어마다 그 언어를 통해 배울 수 있는 고유한 개념이나 관점등이 있다는 것이다. 함수형 언어가 가진 개념들의 정수를 살펴보고 싶다면 Haskell, 병행성과 scalability에 관심이 있다면 Erlang, 객체 지향에 대해 알고 싶다면 Smalltalk 등이 좋을 것이다. 하지만 현재 익숙한 언어 외에 하나의 언어밖에 볼 시간이 없다면 Common Lisp을 볼 것을 추천한다. 그 이유는 Common Lisp이 여러 가지 개념들을 동시에 접할 수 있는 multi-paradigm 언어이기 때문이다. '모든것이 가능하다'라는 말이 Common Lisp을 가장 잘 표현해주는 말이 아닌가 생각한다. 프로그래머는 원하는 모든 것을 할 수 있다. 객체 지향 방식으로 프로그래밍 할 수도 있고, 함수형 언어의 특징을 사용할 수도 있다. 동적 타입에 의존할 수도 있고 정적으로 형을 선언해서 프로그램을 빠르게 만드는 것도 가능하다. 기본적으로 abstraction 레벨이 높아서 표현력이 뛰어난(더 적은 문장으로도 많은 내용을 표현할 수 있다면 표현력이 좋다고 할 수 있다) 반면에, goto나 비트연산, 스택을 다루는 등의 하위 레벨의 표현도 가능하다. Common Lisp을 통해서 다음과 같은 개념들을 익힐 수 있다.

  • atom, symbol, keyword parameter
    => Erlang의 atom, 루비의 symbol, Object-C의 키워드가 당연해 보이게 된다.

  • higher order function, currying, closure, memoize
    => 자바스크립트나 Erlang의 higher order function이나 closure가 쉽게 눈에 들어온다.

  • Object Oriented System, OO 개념의 메타 개념이라 할 수 있는 meta object protocol(MOP)
    => 루비의 MOP는 어린애 장난 수준으로 보이게 된다.

  • exception 개념의 메타 개념이라 할 수 있는 condition system

  • continuation
    => On Lisp을 읽는다는 가정 하에

  • 런타임과 컴파일타임의 구별이 없는 hot code deployment

  • 코드와 데이터의 동일성과 매크로
    => Lisp만의 고유한 특성으로, 가장 살펴볼만한 가치가 있는 개념이라고 생각한다.


Lisp을 보고 나서 다른 언어를 살펴보게 되면 Lisp에서 나온 개념의 하위 개념을 다루고 있는 경우가 대부분이기 때문에 이해가 매우 쉽다고 할 수 있다.

Common Lisp을 살펴보고 싶다면 Common Lisp 가이드를 참고하기 바란다.