Copyright © 2002, 2004 Pascal Costanza. All rights reserved. Permission to copy, transmit, and store this work, unmodified and in its entirety, is granted.
You can use this document as an online reference or print it out. All links are represented as explicit URLs that will be retained when printed. Please send any kind of feedback to costanza@web.de.
한국어 번역: 2004/02/09, 조승모
이 부분은 2002년 8월에 쓰여진 것이다. 따라서 기술된 상황은 이제 과거의 것이지만, 원래 느낌을 유지하기 위해서 시제를 바꾸지 않기로 하였다.
내 현 상황은 다음과 같다. 나는 지난 7년간 자바(Java) 프로그래머로서 일해 왔고, 주로 자바를 확장하는 프로젝트들에 참여해 왔다. 이전에는 보통 워쓰(Wirth) 패밀리에 속하는 언어들(주로 Modula-2와 Oberon)을 사용하고 있었으므로, 처음에는 자바가 이런 언어들에 비해 가지는 장점들이 나를 기쁘게 했다.
작년에 나는, 자바 또한 매우 - 사실, 극단적으로 - 제약된 언어라는 것을 깨닫고, 대안들을 알아보기 시작했다. 나는 Feyerabend 프로젝트(http://www.dreamsongs.com/Feyerabend/Feyerabend.html)에 관여했었으므로, Lisp은 자연스럽게 그 대안들 중 하나로 인식되었다. (Feyerabend 프로젝트은 Richard Gabriel에 의해 시작되었는데, 그는 또한 1980년대 초반에 CL(Common Lisp) 표준화를 제안한 사람들 중의 한명이었다.)
여러 가지 좋은 언어들이 있지만 - 예를 들어 (알파벳 순으로) : gbeta, Objective CAML, Python, Ruby - 나는 단번에, Lisp이 어떤 의미로는 이런 모든 언어들의 모체라는 인상을 받았다. 이렇게 말할 수 있는 주된 이유는, Lisp은 코드와 데이터를 단일하게 취급함으로써, 완전한 계산 이론(a complete theory of computation)을 포함하고 있다는 점이다. (이것은 "단순히" Turing-complete한 것보다 더 강력한 것이다. 더 자세한 정보는 아래의 "Lisp의 기술적 배경" 부분을 참고하라.) 이는 Lisp에는 어떠한 (개념적) 제약도 없다는 것을 의미한다: 만일 다른 언어로 어떤 것을 할 수 있다면, Lisp으로도 할 수 있다. 더 나아가, Lisp은 모든 타입(type)을 수행시간에 체크하므로, 정적 타입 시스템에 의해 방해받는 일도 없다.
이런 관찰을 일반화하면 다음과 같이 말할 수 있다: Lisp의 사고방식에서는, 표현력이 프로그래밍 언어에서 유일하게 가장 중요한 요소라고 본다. 다른 어떤 것도, 이런 표현력의 사용에 장해가 되어서는 안된다. 프로그래밍 언어가 그 자신의 세계관을 프로그래머에게 강요해서는 안된다. 프로그래머가 언어를 자신의 필요에 맞게 수정해야지, 그 반대이어서는 안된다.
이는 나에게 아주 매력적이었고, 그래서 나는 Lisp을 선택하기로 했다. 그러나 Lisp을 좀 더 진지하게 들여다 보기에는, 한가지 문제점이 있다: 인터넷에는 내 필요에 맞는 정말 좋은 소개문서가 없었다. (혹은 내가 찾지 못했을 뿐인지도 모르지만.) 물론 몇개의 문서가 존재하지만, 그들은 보통, 너무 저수준으로 초보자에게 프로그래밍 자체를 가르치는 문서이거나, 너무 학술적이어서 이론적으로나 재미있을 주제들을 다루는 문서들이다. 내가 원했던 것은, 개념들을 이해하고 가능한 빨리 작업할 수 있게 하는, 충분한 배경지식을 제공하는 소개문서였다. ("경험있는 프로그래머를 위한 Lisp" 같은 문서랄까.)
그런 것을 찾지 못했으므로, 나는 가능한 자료들을 통해 스스로 해 나갈 수 밖에 없었다. 이에, 내가 찾은 것들을 정리하여 다른 사람들의 작업을 쉽게 하고자 한다.
왜 이 문서가 "몹시 주관적"인가? Lisp은 복잡한 언어, 혹은 언어들의 패밀리이다. Lisp에 대한 "하나의 결정적 가이드" 따위는 있을 수 없다. 여기에 제시하는 것은, 내가 처음에 봤었으면 좋았을 걸, 하고 지금 생각하는 것이다. 다른 사람들은 아마 다른 접근법을 선호할 것이다. 나는 모든 사람을 위한 모든 정보를 제공하는 소개문서를 만들고 싶지는 않다. 내가 원하는 것은 단지 나와 유사한 사람들에게 유용한 정보를 제공하는 것이다. 적어도, 이것은 나한테는 유용했던 자료였고, 따라서 아마도 다른 어떤 사람들에게도 적합할 것이다.
특히, 이것은 "Common Lisp 21일 완성" 등과 같은 문서가 아니다. 실제로 "Common Lisp 경험"에 대한 감을 잡으려면, 당신은 아마도 여기 제공된 것, 혹은 다른 자료들을 여러번 읽어볼 필요가 있을 것이다. (... 하지만 어떤 진짜 언어를 배우기 위해서도, 사실 21일보다는 훨씬 더 걸린다는 사실을 굳이 강조하지는 않아도 될 것이다. 그렇지 않은가? ;)
Lisp은 긴 역사를 가지고 있다. 1950년대에 발명되었으며, 그후 수십년에 걸쳐 꾸준히 개발되고 개선되어 왔다. 그 과정에서 여러번, Lisp의 여러 변종들이 나타났고, 따라서 Lisp은 하나의 언어라기보다는 언어들의 패밀리라고 말하는 것이 옳다. (만일, C, C++, Objective C, Java, C# 등을 같은 "유사 C" 언어 패밀리라고 생각한다면, 그와 아마 상당히 유사할 것이다.)
1980년대와 1990년대에 들어, 그 중 두개의 변종들이 유일하게 널리 사용되고 쓸만한 언어로서 정착되었다: Common Lisp (CL)과 스킴(Scheme)이다. 스킴은 1970년대에 Gerald Jay Sussman과 Guy L. Steele에 의해, 객체지향을 이해하고 그것을 Lisp에 결합시키려는 시도의 결과로써 발명되었다. 스킴은 개념적 레벨에서, 객체의 일반화로서의 렉시컬 클로저(lexical closure)와, 함수호출과 메시지전송의 일반화로서의 계산과정(continuation)을 도입했고, 기술적 수준에서는 꼬리 호출(tail call)과 꼬리 재귀(tail recursion)를 위한 "극단적인" 최적화를 도입했다. 스킴은 학계가 요구하는 진(眞)과 미(美) - 즉 minimality와 orthogonality - 를 갖춘 아름다운 다이아몬드 같은 것이다. 그러나 그것은, 일상적인 프로그래밍에서 필요하지만 개념적인 아름다움을 해칠 수 있는 많은 실용적인 것들을 포함하지 않고 있다.
위에서 언급된 몇몇 용어에 대한 설명에 관심이 있으면 다음 링크를 참조할 것.
Lexical scope, dynamic scope, (lexical) closures: http://c2.com/cgi/wiki?ScopeAndClosures
Continuations: http://c2.com/cgi/wiki?CallWithCurrentContinuation
Tail calls, tail recursion: http://c2.com/cgi/wiki?TailCallOptimization
1970년대가 끝날 즈음, 여러 Lisp의 변종들이 사용되고 있었다. CL은 이러한 다양함에 대한 해결책으로 시작되었다 - 당시 존재하던 여러 Lisp 들의 장점들을 결합하고 단점들을 피하여 단일한 Lisp 버젼을 제공하겠다는. 또한 CL은 스킴에서 배운 교훈들을 포함하는 더 큰 한걸음이기도 했다. (렉시컬 스코핑(lexical scoping)은 CL에 채용되었다. 계산과정(continuation)은 실용적으로 사용되기에는 너무 복잡하다고 판명되어 포함되지 않았다. 꼬리 재귀에 대한 최적화는 구현기술로서 사용되는 것이 자연스러웠으므로, 표준화에는 포함되지 않았다.)
이를 참조하면 지금 Lisp을 사용하려고 할 때 무엇을 써야 할 지를 결정하기 위한 힌트를 얻을 수 있을 것이다. 만일 실용적인 "real-world" 문제에 적용하기 위해서라면, CL을 골라야 한다. 만일 계산과정(continuation)에 대한 실험을 한다던가 하는 학술적인 연구를 하기 위함이라면, 스킴을 고를 수 있다. 두 언어 모두 여전히 사용되고 있으며, 각각의 사용자 공동체로부터 충분한 지원을 받을 수 있다.
나는 몇가지 이유에서 CL을 사용하기로 결정했다. 그중 몇가지는 이 소개문서 중간에 드러날 것이다. 내가 스킴에서 좋아하지 않는 점은 그것이, 어떤 특정한 프로그램을 표현하는 하나의 옳은 방식이 존재한다, 라는 개념을 가지고 있는 언어라는 점이다. CL은 훨씬 더 유연해 보인다.
하지만, 스킴에 관한 많은 출판물이나 문서들이 CL에도 마찬가지로 적용되므로 - 예를 들어 렉시컬 클로저가 무엇인가에 대한 문서들 - 앞으로 스킴에 대한 몇몇 참고자료도 포함하기로 한다.
(본질적으로, 내 메시지는 이것이다: CL이나 스킴 중에 무엇을 고르는지는 중요하지 않다. 그건 당신의 특정한 필요에 달린 것이다. 그러므로, 맘에 드는 것을 골라라. 사실 두 언어 모두, 연구나 실용적인 일들을 하기에 충분한 유연성을 제공한다. 하지만 이 소개문서의 많은 부분은 CL을 다루고 있으므로, 만일 스킴을 골랐다면 다른 소개문서를 찾아보는 것이 좋을 것이다. 아래의 Link 섹션을 참조하라.)
이 문서를 보는 방법에 대하여: 나는 이 문서를, 순서대로 보는 것이 자연스럽게 만들기 위해 노력했다. 물론, 몇몇 섹션을 건너뛰거나, 특히 파트 I 과 파트 II 간에 왔다갔다 하면서 보는 것은, 당신의 자유이다. 하지만 뒤의 몇몇 섹션은, 당신이 Lisp 예제 코드에 대한 어느 정도의 경험이 있을 것을 요구한다. 이를 위한 충분한 포인터가 본문 중에 제시될 것이다.
CL이 제공하는 것들에 대한 좋은 한페이지짜리 개요는 http://www.lisp.org/table/objects.htm에서 찾을 수 있다. 이는, CL의 일부인 CLOS (the Common Lisp Object System) 의 객체지향적 측면에 촛점을 맞추고 있으나, 그 외의 함수적(functional)이나 행동지침적(imperative) 측면에 대해서도 좋은 요약을 제공하고 있다.
Xanalys에서 제공하는 또 하나의 좋은 요약은 http://www.lispworks.com/products/lisp-overview.html에서 찾을 수 있다.
Lisp의 역사는, 다른 프로그래밍 언어들 - 특히 Algol/C 패밀리의 언어들 - 과 다르다. 따라서 Lisp 공동체는 여러 레벨에서 다른 표현과 용어들을 개발해 왔다. 아래는 혼동을 피하기 위한 몇가지 해명이다.
어떤 Lisp의 기본 함수들(built-in functions)은 좀 웃기게 보이는 이름들을 가지고 있다. 예를 들어, "car"은 리스트의 첫 원소를 구하는 함수이고, "cdr"은 그 외의 원소들을 구하는 함수이다. 리스트는 "cons"라는 함수를 이용해서 만들어지고, "mapcar"는 리스트의 각 원소에 대해 뭔가 처리할 때 사용된다. 특히 알골(Algol) 혹은 워쓰 패밀리 언어들의 사용자들은, 왜 Lisp의 설계자들이 좀 더 자연스러운 이름들 - 예를 들어 "first", "rest", "foreach" 등등 - 을 사용하지 않았는지를 궁금하게 여기곤 한다. 적어도 이것이 내가 이런 이름들에 처음 맞닥뜨렸을 때의 반응이었다. (물론 나만 그런 것은 아니다.)
부디 당신의 자연스러움에 대한 기존관념을 버려주기를. 주로 역사적인 이유로, Lisp은 단지 다른 문화를 가지게 된 것 뿐이다. 예를 들어 마찬가지 방식으로 생각하면, 영어가 프랑스어에 비해 더 자연스러운 언어라고 할 수 있다: 당신의 모국어가 영어일 때만. 이러한 명명법에 익숙해지는데는 하루나 이틀의 Lisp 코딩 경험이면 충분하고, 그 후에는 이것들은 문제가 되지 않는다. CL은 또한 다른 대안적인 이름들도 제공하는데, 예를 들어, "car"과 "cdr" 대신에 "first"와 "rest"를 쓸 수도 있다. 하지만, 아직도 전통적인 이름들을 쓰는 코드를 읽기 위해 준비할 필요는 있다.
같은 원리가 Lisp의 얼핏 과다해 보이는 괄호 사용에도 적용된다. 이는 단지, Lisp 스타일의 들여쓰기(indentation)를 지원하는 에디터를 고르기만 하면 되는 문제이고, 그 후에는 괄호는 잊어도 된다. Lisp에서 정확한 구문사용때문에 걱정할 필요는 거의 없을 것이다. C-like 언어들이나 Pascal 에서도, 꺽쇠괄호나 begin/end 등을 제어구문들(if, while, switch/case 등등)과 잘못 사용하여 문제가 되는 경우가 있음을 상기하라. 하지만 당신이 이들 언어의 경험있는 프로그래머라면, 이런 일들이 문제가 되지는 않을 것이라고 믿는다. Lisp에도 같은 논리가 적용된다. 이는 단지 익숙해짐의 문제이다.
몇몇 사람들은 아직도 Lisp은 자료구조로서 리스트만을 제공한다고 생각한다. 이는 그들이 매우 오래된 Lisp 언어들만을 배웠고, Lisp이라는 이름 자체가 "List Processing"의 줄임말이기 때문이다. 사실 CL은 다른 언어들에서 제공되는 모든 유용한 것들을 제공하는데, 예를 들면 C와 유사한 구조체, 배열, 다차원배열, 객체, 문자열 등등을 포함한다.
유사하게, 몇몇 사람들은 아직도 Lisp이 인터프리터(interpreted) 언어라고 생각한다. 사실 현존하는 Lisp 구현들 중에서 순수한 인터프리터 시스템은 거의 없다고 해야 맞는다. 모든 중요한 구현들은 직접 머신코드를 생성하는 컴파일러를 포함하고 있다. 따라서 Lisp 역시 일반적으로 compile-time과 runtime을 구별한다. 이 차이는, 예를 들어 매크로(macro) 프로그래밍을 할 때 중요하게 된다.
하지만 여전히, 여기에는 소위 "컴파일 언어"인 C 나 Pascal 등과는 다른 점이 있다. 대부분의 경우, 당신은 Lisp 프로그램을 명시적으로 컴파일하고, 그 후에 수행하는 식으로 일하지 않는다. 대신 Lisp 개발 환경과 상호작용하면서, Lisp 코드를 그때그때 필요할 때마다 컴파일한다. 따라서 Lisp은 개념적으로는, 일종의 just-in-time 컴파일 언어에 가깝다. Lisp은 예를 들어 Smalltalk와 같은 interactive한 특성을 가진다. (Smalltalk는 어느 정도는 Lisp 언어 패밀리에 속한다고 볼 수도 있을 것이다.)
가장 중요한 점은, Lisp은 몹시 효율적인 언어라는 점이다. CL 회사들의 수년에 걸친 노력의 결과, 그들은 훌륭한 성능을 제공하고 있다.
(자바를 위한 현대적인 IDE들, 예를 들면 JBuilder, NetBeans, Eclipse 등을 보면, 그들이 Lisp이나 Smalltalk가 제공하는 수준의 상호작용성(interactivity)에 계속 근접해 가는 것을 볼 수 있다. 즉, 이러한 점이 알골이나 C 계열의 언어들의 공동체에서도, 가치있는 것으로 간주되고 있음을 알 수 있다.)
Lisp의 성능적 측면을 자바와 C++과 비교한 최근의 연구는 다음 URL에서 찾을 수 있다: Erann Gat, Lisp as an Alternative to Java, http://www.flownet.com/gat/papers/. (만일, 저 논문에서 언급된 자바 프로그래머들의 경험치가 문제라고 생각한다면, 좀 더 명확한 설명을 위해 다음을 참조하라. http://www.flownet.com/gat/papers/ljfaq.html)
Lisp의 설명에서 자주 사용되는 용어의 하나는 "폼(form)"이다. 이는 Lisp 구문의 기본 요소이다. 다음 예를 보자:
(format t "Hello, World")
이것은 콘솔에 "Hello, World"를 출력하는 폼이다. (파라메터 "t"는 CL에서 "참"을 나타내는 표준 이진(boolean) 값이고, format 함수에 의해서 표준출력으로 해석된다.) 따라서 "폼" 이란 용어는 다른 언어에서의 식(expression)이나 문장(statement)의 개념을 나타낸다. (Lisp 순수주의자는, Lisp에는 문장은 없고 단지 식 뿐이라고 주장하겠지만, 이런 구분은 실제로는 별 의미가 없다. 다른 언어에서 식과 문장 간의 경계는 소위 말하는 ""expression statements" - 값은 버려지는 식 - 을 허용하는 것에 의해서 모호해 진다. Lisp에서도 같은 일이 가능하고, 더 나아가 Lisp에서는 값을 갖지 않거나 "미정의값"을 가지는 식도 정의할 수 있다. 따라서 실제로는, Lisp과 다른 언어들은 같은 구분을 하며 비슷한 방식으로 그 구분을 흐린다. 단지 무엇이 기본 동작인가 하는 것만이 다를 뿐이다.)
Lisp에는 또한, "특수 폼(special forms)"이라고 불리는 것들도 있다. 이 용어를 이해하려면, 우선 Lisp에서는 보통 모든 폼들은 함수 호출이라는 것을 알아야 한다. 위의 예는 "format"이라는 함수를 "t"와 "Hello, World"라는 인자(parameter)를 가지고 호출한 것이다. 다음 예는 함수 "foo"를, 인자 "bar"와 "goo"로 호출한 예가 될 것이다.
(foo bar goo)
그러나, Lisp에서는 함수 호출이 아닌 폼들도 있다. 그것은 매크로(macro)이거나 "특수 폼" 이다. 특수 폼은 단순히, 매우 한정된 기본 연산자(built-in operators)의 집합을 나타낸다. (이것들은 CL 구현에 의해 특별하게 취급되는 것이다. 기본 함수나 매크로들은 특수 폼이 아니라는 것에 주의하라!) 어떤 폼이, 함수 폼인지 매크로 폼인지 특수 폼인지 하는 것은, 항상 그 첫 원소를 보면 알 수 있다. 여러가지 폼 사이의 이러한 구분은, 이론적으로뿐만 아니라 실제적인 중요성도 가지지만, 미리 이런 것을 걱정할 필요는 없다. 때가 되면 바로 그 차이를 알 수 있게 될 것이고, 그것을 어떻게 다뤄야 하는지도 쉽게 알게 될 것이다.
Lisp 공동체 내에는 예전부터, 그리고 아직까지도, 작은(small) 언어 대 큰(big) 언어 간의 큰 논쟁이 있다. 이는 다른 공동체에서 온 사람들에게 혼란스러울 수도 있다. (적어도 내게는 그랬다.) 과연 무엇이 문제인가?
Lisp의 좋은 점 중의 하나는, built-in 함수들과 유저가 정의한 함수들을 명시적으로 구분하지 않는다는 점이다. 아래의 두 예를 보자.
(car mylist)
(cure mylist)
함수 car는 Lisp에서 미리 정의된 것이고, 함수 cure는 표준에 정의되지 않았으므로 아마 사용자 정의 함수일 것이다. 중요한 점은, 이 차이를 구문에서는 알 수 없다는 것이다. 이 점을 다음의 자바 예제와 비교해 보라.
synchronized (lock) {
beginTransaction(...);
...
endTransaction(...);
}
자바의 synchronized 문장은 자바의 built-in 구문으로, 한 특정 객체와 동기화를 이루는 코드 블럭을 정의하는데 사용된다. 이 예제의 트랜잭션 관련 메소드들 역시, 아마도 트랜잭션과 관련된 시맨틱스를 존중하도록 작성된 코드 블럭을 정의하고 있을 것이다. 하지만, 이러한 메소드들이 자바의 built-in 구문이 아니라는 것은 바로 알 수 있다. (Lisp 역시 블럭 기반 구문 - 아, 미안 - 폼들을 제공한다는 말은 사실이다; 또한 Lisp의 built-in 구문처럼 보이는 당신 자신의 블럭 구문을 정의하는 것도 가능하다는 것도 사실이다.)
이제, 작은언어 대 큰언어의 이슈를 이해하기 위해, 이러한 차이를 고려해야 한다.
자바나 그와 유사한 프로그래밍 언어를 표준화한다는 것은 무슨 뜻인가? 우선 언어를 표준화해야 하고, 또한 적어도 라이브러리들의 일부를 표준화해야 한다. 위에서 설명한 대로, 그러한 언어들에서는 built-in 구문들과 라이브러리 간에는 분명한 차이가 있다. 따라서 이들은 분명히 다른 두개의 작업이 된다.
Lisp의 경우에는 그렇지 않다. 여기는 built-in 구문과 라이브러리 간에는 차이가 없으므로, 라이브러리의 표준화가 본질적으로 언어 표준화의 일부분이 된다. (어떤 의미에서는, 그 역도 성립한다.)
따라서 작은언어 대 큰언어의 이슈는, (Algol 이나 C 계열의 용어로 말하자면) 작은 라이브러리 대 큰 라이브러리의 이슈가 된다. 그게 전부다!
따라서 이는 다음과 같이 정리될 수 있다: 스킴은, 아주 작은 표준 라이브러리만을 제공한다는 의미에서 작은언어이고, 반면 CL은 많은 유용한 표준 라이브러리를 제공하므로 큰언어이다. 하지만, 두 언어의 핵심 부분만을 살펴보면, 그 둘은 크기 면에서 큰 차이가 없다. (또한, 실제적인 스킴 구현들을 살펴보면, 그들 역시 풍부한 라이브러리들을 제공하고 있다. 하지만 그것들은 표준화 된 것은 아니다.)
또한, 작은언어 대 큰언어의 이슈로 귀결되지 않는 다른 개념적인 차이들도 있다. 이는 스킴과 CL 사이의 선택이 단지 언어 크기의 문제만이 아님을 말해 준다.
예를 들어, 때때로 스킴을 "Lisp-1"이라 하고 CL을 "Lisp-2"라고 하는 글을 만날 수 있다. (혹은 스킴을 one-cell 시스템, CL을 two-cell 시스템 이라고 하거나.) 이는 변수의 값과 함수 정의가, 스킴에서는 같은 식으로 저장되는데, CL에서는 다르게 저장된다는 것을 말하는 것이다. 두 언어 중 어떤 것을 가지고던 실제적 프로그램을 작성할 수 있으므로, 실용적 목적으로는 전혀 상관없다. Lisp-1 방식이건 Lisp-2 방식이건, 그 방식을 선택한 결과를 다룰 수 있는 방법이 제공된다. 실제로 이런 문제에 맞닥뜨릴 때까지 이런 문제는 무시해도 되며, 다시 말하지만, 그럴 때가 되면 쉽게 그걸 다룰 수 있는 방법을 알 수 있게 될 것이다.
("Lisp-1" 방식은 어떤 종류의 함수적 프로그래밍 스타일을 더 쉽게 해 주며, "Lisp-2" 방식은 매크로 프로그래밍의 오류가능성을 줄여주고 객체지향 기능의 사용을 더 쉽게 한다. 정확한 세부사항은 좀 난해하다. 만일 기술적인 세부사항에 대해 관심이 있다면, 다음 문서를 참조하라.
Richard P. Gabriel and Kent M. Pitman, Technical Issues of Separation in Function Cells and Value Cells, http://www.dreamsongs.com/Separation.html)
아래의 매우 잘 쓰여진 두개의 페이퍼에서, CL과 스킴을 포함하는 Lisp의 전체 역사에 대한 충분한 정보를 얻을 수 있다. 어떤 부분에서는 범죄 소설을 읽는 기분까지 느끼게 할 정도. - 강추!
John McCarthy: History of Lisp (ACM SIGPLAN History of Programming Languages Conference, 1978), http://www-formal.stanford.edu/jmc/history/lisp.html
후자는 Lisp에 대한 상당히 완전한 서지정보도 포함하고 있다.
(이들과, Herbert Stoyan이 작성한 Lisp의 역사에 대한 다른 페이퍼들을 혼동하지 말도록. 내가 보기에 그것은 읽기도 어렵고, 좀 이상한 결론에 도달하고 있는 것 같다.)
전술한 대로, Lisp은 코드와 데이터를 동일한 방식으로 다룸으로써 계산에 대한 완전한 이론을 포함하는데, 이것이 Lisp을 그렇게 강력하게 만드는 주요한 원천이다. 아래의 두 글은 이것이 실제로 무엇을 의미하는지에 대한 좋은 설명을 해 주고 있으므로 추천할 만 하다. (그들은 "meta-circular interpreters"의 개념을 다루고 있다. 겁먹지 말길. 보기처럼 복잡한 것은 아니다.) Lisp의 힘을 알기 위해서는 적어도 처음 것은 꼭 읽어야 한다고 생각한다.
... 그리고 또한, 이 글은 Lisp의 기본적인 폼들에 대한 좋은 설명도 제공한다.
위의 "The Roots of Lisp"이란 글이 맘에 들었고, meta-curcular interpreters에 대해 더 알고 싶다면, 아래의 글에서 더 깊은 토의와 그걸 사용해서 할 수 있는 일들에 대한 비전을 얻을 수 있다. 그와 더불어, 렉시컬 클로저(lexical closure), 다이나믹 바인딩(dynamic biding) 등에 대한 유용한 사실들을 배울 수 있다.
(이 글은 속편이 있을 것처럼 말하고 있지만, 그런 것은 쓰여지거나 발표된 적이 없다. 그러므로 찾지 말 것: 아니면, 연습삼아 한번 완성시켜 보던가. ;)
다음 링크에서 CL의 구현들에 대한 리스트를 찾을 수 있다: http://alu.cliki.net/Implementation. 나는 이 중 일부만을 좀 자세히 살펴봤을 뿐이다. 많은 Lisp 프로그래머들은 개발환경으로 emacs나 xemacs를 선호하지만, 나는 좀 더 현대적인 IDE를 선호하므로, 이 점이 내 선택 가능성을 많이 축소시켰다. (이 점에 대해서 논쟁하지 말 것. 이는 단지 개인의 취향의 문제이므로. 만일 emacs를 이용하여 IDE를 설정하는 방법을 알고 싶으면, http://cl-cookbook.sourceforge.net/windows.html이 가장 유용한 정보를 제공할 것이다.)
애플 매킨토시(Mac OS X)를 위한 추천: LispWorks for Macintosh는 뛰어난 IDE이고, 그 개인용 버젼은 무료로 다운로드 가능하다. Macintosh Common Lisp도 매우 좋은 IDE이지만, 이는 LispWorks에 비해 Mac OS X에 잘 통합되어 있지 못하다. Digitool에서는 MCL의 30일간 무료 시험 사용을 허용하고 있다. CLISP, ECL, OpenMCL, SBCL 등이 Mac OS X 용으로 존재하나, 그들은 IDE가 없다는 (내게는) 중요한 문제가 있다. Franz, Inc. 는 Mac OS X를 위한 Allegro Common Lisp의 IDE를 제공할 예정이지만, 확정된 계획은 없다. 따라서 너무 기대하지 말 것.
나는 LispWorks와 Macintosh Common Lisp을 둘 다 사용한다. 하지만 이것은 하나를 고르라는 추천이 아니다. 자신의 요구사항은 자신이 체크하도록!
여기서는 CL의 여러 측면을 이해하는데 도움이 될 수 있는 힌트와 참고자료들을 주로 소개한다. (주로 표준 라이브러리에 대한 이야기다.)
일상적인 프로그래밍 시에, 당신은 함수정의나 명확한 구문정의 등등을 찾아 볼 수 있는 참고자료들을 필요로 하게 된다. 어떤 사람들은 책 형태를, 어떤 사람들은 온라인 형태를 선호한다.
현재의 CL 표준은 1995년에 발표된 ANSI 표준에 정의되어 있고, 그것이 CL의 최종적인 정의이다. 그러나 그것은 스펙만을 다루고 있고, 예를 들어 그 배경의도 등이 전혀 설명되어 있지 않기 때문에 읽기 어렵다.
1990년대 초에, Guy L. Steele는, "Common Lisp the Language" 라는 책의 2판을 편집했는데, 그것은 그 시점에서의 CL 표준화에 대한 리포트였고, 최종 ANSI 표준과 아주 가까운 형태를 가진다. (이 판은 1980년대에 발표된 초판의 개정판이므로 일반적으로는 CLtL2 라고 불린다. 초판 - CLtL - 은 더 이상 사용되지 않는다.) 비록 CLtL2가 ANSI CL의 몇몇 특성을 다루지 못하고 있고, 또한 작은 차이점들도 있지만, 단순한 스펙 이상의 좋은 설명을 제공한다는 큰 장점이 있으므로, 일반적으로 개론서로서 추천할 만 하다. 이 장점은 스펙에 대한 이해를 아주 쉽게 한다. 그렇지만 여전히 ANSI 스펙을 꼭 가지고 있을 필요가 있는데, 이는 특정 정의의 정확한 세부사항들을 참조하기 위해서이다. 또한 ANSI 문서는 매우 유용한 용어집을 포함하고 있다.
다행히도 ANSI CL과 CLtL2 둘 다, HTML(과 PDF,PS) 문서의 형태로 온라인으로 구할 수 있다. HTML 버젼까지 다운로드하여 사용할 수도 있다. 다음이 그 링크들이다.
(이 "HyperSpec" 버젼의 편집자는, 이것이 확실한 스펙이 아니라, 단지 공식 ANSI 문서에서 추출된 것이라는 점을 강조한다. 하지만 이것은 단지 법적인 문제일 뿐, HyperSpec을 거의 확실하게 믿어도 별 문제는 없을 것으로 생각된다. 사실, 이것은 같은 TeX 소스 코드에서 생성되었다.)
다른 스타일의 ANSI CL에 대한 링크가 Franz, Inc. 에 의해 제공되고 있다: http://www.franz.com/support/documentation/6.2/ansicl/ansicl.htm
(조심할 것: CLtL2는 소위 "Series" 매크로와 "Generators and Gatherers"라는 lazy iterators를 구현하는 부분에 대한 부록을 포함하고 있다. 이들은 ANSI CL에 포함되지 않았다. 이들이 왜 CLtL2에 포함되었다가 ANSI CL에서 다시 빠지게 되었는지는 알 수 없지만, 아마도 그것들은 너무 실험적이었던 것으로 생각된다. "Series" 매크로는 http://series.sourceforge.net에서 별도의 라이브러리로 구할 수 있다.)
만일 인쇄물 형태를 선호한다면, 구하기 위해서 좀 수고를 해야 한다. CLtL2는 절판인 듯 하지만 Ebay에서 중고를 구할 수 있을 것이다. Amazon과 같은 몇몇 (온라인) 서점에서는 아직 구할 수 있다. 또한 출판사의 웹사이트(http://www.bh.com/digitalpress/)에서 구입을 시도해 볼 수도 있을 것이다.
ANSI 문서는 http://global.ihs.com/에서 구할 수 있지만, 1000페이지가 넘기 때문에 특별히 편하거나 유용할 거라고는 생각되지 않는다. 더구나 거의 $400이나 하는 가격은 너무 비싸다.
Paul Graham의 "ANSI Common Lisp"은 좋은 책이지만, 참고 목적으로는 그렇게 유용하지는 못한 것 같다. 일반적으로 - 즉 자바 언어같은 경우 - 나는 인쇄된 스펙을 선호하지만, CL의 경우에는 전자문서 형태로 충분하고 생각한다.
Paul Graham은 "ANSI Common Lisp"이라는 ANSI Common Lisp에 대한 좋은 책을 썼다. 잘 읽히는 책이고, 또한 다행히도 저자는 첫 두 챕터를 그의 홈페이지에서 제공하고 있다. 두번째 챕터는 특히 권할 만한데, 이는 CL로의 프로그래밍이 어떠한지를 직접 경험할 수 있는 좋은 예제들을 제공한다.
그의 스타일이 맘에 든다면, 그 페이지의 링크를 통해 책을 구입할 수 있다.
이후로 당신이 찾고자 하는 대부분의 정보는 CLtL2, 그리고(또는) ANSI 스펙에서 쉽게 찾을 수 있을 것이다. 아래는, 내 생각에 조금 찾기 힘들지도 모를 몇몇 정보들에 대한 것이다.
파트 II의 나머지 부분들을 완전히 이해하기 위해서는, "The Roots of Lisp" (http://www.paulgraham.com/rootsoflisp.html) 이나, "ANSI Common Lisp Chapter 2" (http://www.paulgraham.com/lib/paulgraham/acl2.txt ) 를 읽었거나, 혹은 다른 방법으로 Lisp 예제 코드들을 접해 보았어야 한다.
조만간에 다음과 같은 이슈들을 접하게 될 것이고, 이들은 다시 다른 작은 이슈들과 연관될 것이다.
(람다 식(Lambda expression)은 이름을 갖지 않는 함수이다. 이름을 가지는 함수들은 다른 언어에서의 프로시져, 함수, 메소드들과 유사하다. Lisp의 이름을 갖지 않는 함수는 Smalltalk에서의 block이나, 어느 정도는 자바의 anonymous inner class 와 유사하다. 명확한 세부사항은 CLtL2, 챕터 5.2 ("Functions")와 그 아래를 참조하라.)
CLtL2에서, format은 챕터 22.3.3 ("Formatted Output to Character Streams")에 설명되어 있다. ANSI 스펙은 챕터 22.3 ("Formatted Output")에서 이를 다루고 있다.
일반적으로 이는 문제가 되지 않지만, 다른 프로그래밍 언어로 쓰인 어플리케이션과의 상호작용이 필요할 때는 문제가 될 수도 있다. Allegro Common Lisp, CLISP, LispWorks 등은 유니코드 지원을 제공하지만, 다른 구현들은 적어도 내가 아는 한 (아직) 이를 제공하지 않는다. (그러나 물론, 스스로 유니코드 지원을 구현할 수도 있다 - Lisp의 충분한 유연성은 이런 목적을 위한 것이기도 하다. ;-)
문자열 지원에 대한 자세한 사항이 필요할 경우, CLtL2, ANSI 스펙과 함께, 당신이 선택한 CL 시스템에서 제공되는 문서를 참조하라.
CL의 매크로 기능은 이 언어의 가장 흥미있는 점 중의 하나이다. 이는 당신 자신의 프로그래밍 언어 구문을 정의할 수 있는, 단순한 함수 이상의 매우 강력한 수단이다.
매크로는 그 강력함과 이해하기 힘들어 보인다는 점 때문에, 일반적으로 고급 주제로 인식된다. 하지만 나는 그것이 개념적으로 매우 단순하고 쓰기 쉽다는 점을 발견했다.
매크로에 대한 매우 뛰어난 설명과 사용법은 Paul Graham의 "On Lisp" 에서 찾을 수 있다. 이는 절판되었지만, 다음 URL에서 무료로 다운받을 수 있다.
아래는 내 생각에 주의가 필요하다고 생각되는 부분들이다.
따라서, "매크로"라는 용어를 보자마자 물러서고 싶더라도, 한번 더 생각해 보라. 이는 아마도, 다른 저개발 프로그래밍 언어에서 생긴 편견 때문일 수 있으니까. ;-)
(본 문서에서는 C의 매크로에 대해서는 더 이상 언급되지 않으므로, 이하에서의 "매크로"는 항상 Lisp 매크로를 칭한다.)
다음은 "cure"를 "car"하고 동일하게 정의하는 매크로 이다.
(defmacro cure (argument)
(list 'car argument))
따라서, (cure a) 라는 코드를 만날때 마다, 이 부분은 컴파일 타임에 (car a) 로 대치된다. 즉, 매크로의 결과가 실제 수행에 사용될 코드를 결정한다.
(이 예는 단순히 매크로 정의의 한 예를 보이기 위해 만든 것이다. 이는 매크로를 사용하는 좋은 예제는 아니다: "cure"는 함수로 정의하는 것이 더 좋을 것이다.)
여기서 볼 수 있는 것은, 매크로는 Turing-complete하다는 점이다. CL의 모든 표현력이 컴파일타임에 매크로를 이용한 새 코드의 생성을 위해 사용될 수 있다. 매크로는 다른 매크로 확장이나 함수 등을 포함할 수 있으며, 물론 반복이나 재귀호출, 조건문 등도 전부 사용가능하다. 심지어 매크로 적용의 결과로 생성된 코드에 다시 매크로가 포함될 수도 있으며, 이는 반복된 매크로 확장으로 이어지고, 결과 코드가 순수하게 함수 사용만으로 구성될 때까지 반복된다.
따라서 결과적으로 CL의 매크로는 C++의 템플릿 메타 프로그래밍(template meta-programming)과 같은 목적으로 사용될 수 있다. 특히 그것은 Generative Programming에 사용될 수 있다! (Generative Programming은 본질적으로, 특정 도메인에 맞는 언어의 정의를 다루는데, 그러한 언어의 컴파일러들은 일반목적 프로그래밍 언어 코드를 생성한다.)
전술한 대로 Lisp의 대부분의 폼은 함수 호출이다. 따라서 (car mylist) 는 mylist가 가리키는 리스트의 첫번째 원소를 구한다. 경우에 따라서는 폼의 값을 구하고 싶지 않은 때도 있다. (그것을 함수 호출로 해석하고 싶지 않은 경우를 말한다.) 예를 들어, (car (a b c))에서, (a b c)는 기본적으로 세 원소의 리스트가 아니라, 두 인자 b와 c를 가지고 함수 a를 호출하는, 함수 호출로 간주된다. (a b c)의 값을 구하고 싶지 않은 경우, 특수 폼인 "quote"를 사용해서 다음과 같이 나타내야 한다: (car (quote (a b c))). 이제 이 전체의 식은, 리스트 (a b c)의 첫 원소인"a"로 계산된다. (quote ...)은, 함수 호출이 아니라, Lisp의 기본 기능인 특수 폼이라는 점에 주의하라.
quote는 Lisp에서 아주 자주 사용되므로, 인용부호를 써서 다음과 같이 나타낼 수 있다: (car '(a b c)) 라는 표현은 (car (quote (a b c))) 하고 완전히 똑같다. 그런데, 인용부호는 변수의 값을 구하는 것을 막는데도 쓰일 수 있다. Lisp에서의 변수는 (다른 언어에서와 마찬가지로) 단순한 이름이므로, 예를 들어, (car a) 라고 쓴다면, 변수 a 에 의해서 참조되는 리스트의 첫 원소가 구해진다. 이와 같이 변수의 값이 구해지는 것을 막기 위해서는, (quote a)나, 혹은 짐작했겠지만, 'a 라고 쓰면 된다.
이제, 몇몇 식과 변수들의 값을 구해서 그것들을 연결하여 리스트로 만들어야 할 경우가 있다. Lisp에서 가장 간단한 방법은 다음과 같다: (list a (car b) c)로 쓰면, 이는 a, (car b) 그리고 c의 값을 각각 구하고, 이것들이 연결되어 하나의 리스트가 된다. 반면, "list" 특수 폼의 몇몇 인자들의 값만이 구해져야 할 경우도 있다. 이는 quote를 선택적으로 사용함으로써 해결될 수 있다. 예를 들어 (list a (car b) 'c) 의 경우, a 와 (car b) 의 값은 구해지지만 c는 아니다. 'c의 값을 구한 결과는 c라는 심볼 자체가 되어, 리스트의 마지막 원소는 항상 c가 된다.
어떤 경우에는 인용(quote)되는 - 즉 값이 구해지지 않는 - 식의 수가 값을 구해야 하는 식의 수보다 많을 수 있다. 역인용부호 구문은 이럴 경우 유용하다: 그것은 기본 해석을 뒤집어서, 모든 것의 값을 해석되지 않도록 만든다. 그에 대한 예외들은 별도로 표시되어야 한다. 따라서 다음 예 `(a b c ,(car d)) 는, (list 'a 'b 'c (car d)) 와 정확하게 같다 - 심볼 a, b, c 의 값은 구해지지 않지만, (car d) 의 값은 구해진다. 역인용부호 구문 안에서, 값을 구해야 하는 식이나 변수는 쉼표(comma)로 표시되어야 한다. (이는 일반 인용 구문으로는 구현할 수 없다. 만일 '를 사용한다면, 그에 따르는 모든 식이 인용되는 것이고, 그 일부분의 값을 구하기 위해 예외를 지정할 수는 없다.)
따라서 본질적으로, 역인용부호 구문은 어느것의 값을 구하고 어느것은 구하지 않을 것인지에 대한 기본 해석을 뒤집는 정도의 편의를 제공하는 것 뿐이다.
이제 위에서 주어진 매크로 예제를 다시 살펴보자.
(defmacro cure (argument)
(list 'car argument))
위에서의 역따옴표에 대한 설명을 참조하면, 이 정의가 다음의 것과 동일함을 알 수 있을 것이다.
(defmacro cure (argument)
`(car ,argument))
이로서 매크로와 관련하여 역따옴표 구문이 왜 그렇게 유용한지 바로 알게 되었을 것이다: 매크로 정의의 몸체를, 그것이 만들어 낼 코드와 더 유사하게 만들어 준다. ((cure mylist)는 컴파일 시에 (car mylist)로 바뀐다는 것을 기억하라.)
다음은 역따옴표 구문의 역사에 대한 매우 좋은 논문이다.
Alan Bawden, Quasiquotation in Lisp, http://citeseer.nj.nec.com/bawden99quasiquotation.html
스킴은 소위 "청결한 매크로(hygienic macros)" - 혹은 짧게 "청결(hygiene)" - 이라 불리는 개념을 제공하는데, 이를 통해 이런 변수 이름 충돌을 피한다. 하지만 그들은 청결한 매크로를 너무도 쉽게, 매크로에 대한 개선으로 설명한다는 문제가 있는 것 같다. 표준적인 주장은 다음과 같이 전개된다: "CL 매크로는 변수 이름 충돌이 있을 수 있다; 청결은 이를 피한다; 따라서 프로그래밍이 더 안전해 진다." 이 주장은 청결이 어느 정도는 렉시컬 클로져의 개념을 닮았다는 점에 의해서도 지지된다.
그러나 이는 완전히 사실은 아니다. 경우에 따라, 같은 변수 이름을 만드는 것이 정말 필요한 경우도 있고, 그때 청결은 이를 필요 이상으로 어렵게 만든다. 더우기 Paul Graham이 자기 책에서 보였듯이, 변수 이름 충돌을 피하는 것은 아주 간단하므로, 이 문제의 해결이 처음부터 중요한 이슈라고 하기도 어렵다.
즉 내 주장은 이렇다: CL에서는 안전한 매크로를 쓰는 것이 쉽기 때문에, 청결은 필요하지 않고 따라서 제공되지도 않는다. 정말로 이론적인 측면에서 이 이슈에 관심이 있는 것이 아니라면, 청결한 매크로의 개념에 대해서는 무시해도 좋다.
(스킴에서는 상황이 조금 다른데, 이는 스킴이 변수와 함수 정의를 같은 식으로 저장하기 때문이다 - 위에서 말한 Lisp-1 이다. 이는 실수로 함수 정의가 충돌할 가능성이 CL보다 크다는 것을 뜻한다. 따라서 그 공동체에는 이 이슈의 해결이 더 절실하다. 매크로의 청결성에 관한 여러 문제에 대해서는 다음의 좋은 논문을 참조하라.
연습으로서, 저 논문에서 제시된 예들이 왜 "Lisp-2" - 따라서 CL - 에서는 문제가 안되는지, 그리고 어떤 상황에서 고의적 변수 이름 충돌이 실제로 필요하게 될 지를 궁리해 보는 것도 좋을 것이다. 위에서 소개한 Gabriel과 Ptiman의, 함수 셀(cell)과 값 셀의 구분에 대한 논문도 참고하라.)
Common Lisp Object System(CLOS)은 ANSI CL의 일부분이다. 하지만 표준에 늦게 더해졌기 때문에, 어떤 회사들은 구현에 시간이 걸렸다. 오늘날에는 거의 모든 구현들이 CLOS에 대한 지원을 제공한다. (어떤 회사들은 이를 자기네 CL 구현의 뛰어난 기능으로 광고하기도 하기에, 어떤 때는 이런 점이 좀 혼란스럽게 느껴진다. 정확하게 말하면 한 CL 시스템은, CLOS에 대한 완전한 구현을 포함하지 않는 한, ANSI 표준을 구현하고 있다고 주장할 수 없다.)
CLOS는 클래스, 서브클래싱(subclassing), 다중 상속, 다중 메소드, 전/후/주변 충고(before-, after- and around advices)들을 포함한다. 이것만으로도 다른 객체지향 언어들이 제공하는 기능들을 충분히 넘어선다.
이 외에도 소위 "Meta-Object Protocol"(MOP)라고 불리는 기능이 추가되었는데, 이는 런타임시에 클래스 계층구조와 메소드 정보 등을 살펴보고 조작할 수 있도록 하는 기능이다. MOP는 ANSI CL 표준의 일부분은 아니지만 사실상의(de-facto) 표준이 되었는데, 이는 거의 모든 CLOS/MOP 구현들이 같은 소스에서 나왔기 때문이다. (CLOS와 MOP는, 자바 언어와 그의 reflection API의 경우와 같이 서로 분리된 것이 아니고, MOP가 CLOS를 포함하는 집합이라고 생각할 수 있다.)
CLOS와 MOP에 대한 훌륭한 개요는 다음 논문을 참조하라.
CLOS의 설계 근거(design rationale)는 다음 논문에 나와 있다.
Jeff Dalton은 (MOP 부분 외의) CLOS에 대한 간략한 가이드를 http://www.aiai.ed.ac.uk/~jeff/clos-guide.html 에서 제공한다. MOP에 대한 참고자료는 ANSI HyperSpec과 유사한 스타일로 http://www.lisp.org/mop/에서 제공된다. (외부로의 링크는 대부분 깨져 있지만, 참고자료 자체는 볼 수 있다.)
Barry Margolin은 comp.lang.lisp에서 좋은 경험법칙을 제공했다:
어떤 것이 기본 CLOS에 속하는지, MOP에 속하는지를 분간할 수 있는 좋은 방법은, 그 인자 중에 꼭 클래스, 메소드, 혹은 generic-function 객체이어야 하는 것이 있는지를 보는 것이다. MAKE-INSTANCE나 CHANGE-CLASS 같은 것들은 이 부류에 안 들어간다; 이들이 클래스 객체를 받지만, 그들은 또한 클래스 이름들도 받는다. MOP 함수들은 일반적으로 이러한 편리한 계층(layer)를 제공하지 않는다."
MOP로 가능한 일에 대한 자세하고 인상적인 예제는 다음 논문에서 찾을 수 있다.
추가적 주의사항:
그러나 CLOS에서는, 사용자 정의 가능한 "메소드 조합(method combinations)"에 의해 이런 상황을 보충할 수 있다. 프로그래머는 항상 어떤 메소드가 사용되는지를, 상황별로(case-by-case) 명확하게 정의할 수 있다. (물론 이를 런타임에 수정할 수도 있다.) 즉 CLOS는 이름 충돌을 피하기 위한 매우 높은 수순의 유연성을 제공한다. 슈퍼클래스들의 토폴로지 순서를 이용하는 방법은, 단지 시스템이 제공하는 하나의 가능성으로 생각하면 된다.
다중 상속에 의해 생기는 이름 충돌은 개념적인 이유로 어느 언어에서건 간에 다루기 어려울 수 밖에 없다. 또 다시 CL은 프로그래머에게, 일어날 수 있는 어떤 상황이건 대처할 수 있도록, 필요한 모든 표현력을 제공하는 방법을 택한다. 예를 들어 이를 자바의 경우와 비교해 보라: 자바의 인터페이스 다중 상속은 이름 충돌을 야기할 수 있고, 그에 대한 해결책은 전혀 존재하지 않는다.
LOOP는 CL의 또 하나의 표준 기능으로서, Algol/Wirth 언어들과 유사한 스타일로 반복(iteration)을 표현할 수 있게 해 준다. 다음은 10개의 별을 찍는 loop의 예이다.
(loop for i from 1 to 10
do (format t "*"))
여기서도 역시, CLtL2는 좋은 정보소스이다. 그러나 당신은 곧 loop를 사용하는데 있어서, 수많은 다른 스타일의 가능성과 결합들이 존재한다는 것을 알게 될 것이다. 내 생각엔, 모든 이들 세부사항을 배우는 것은 너무 많은 시간이 걸리고 별로 의미가 없다는 느낌이다. LOOP 기능의 목적은 분명히, 어떤 종류의 "자연스러운" 영어처럼 보이는 반복식들을 표현할 수 있게 하는 것이고, 이것이 소스코드의 이해를 쉽게 하는 경우들도 존재하는 것은 사실이다. (이는 또한, CL에 포함되어 있는 domain-specific 언어의 좋은 예이다 - 반복 이라는 도메인.)
다음은 더 재미있는 LOOP 기능의 사용예이다. (Matthew Danish 작.).
(defun fibonacci (n)
(loop for a = 1 then (+ a b)
and b = 0 then a ; stepping in parallel
repeat n
collect b))
보는 바와 같이, LOOP를 사용하는 의도된 방법은, 단지 반복을 표현할 수 있는 방법을 한번 추측해 보고, 그것이 되는지를 보는 것이다. 만일 안된다면, CLtL2나 ANSI 스펙에서 세부사항을 찾아보거나, 좀더 Lisp 다운 방식으로 반복과 재귀호출을 표현하는 것이다. (do, dotimes, mapcar 등등을 사용해서.)
하지만, 이건 단지 내 추측일 뿐이고, LOOP 기능 설계자의 참의도는 알 수 없다. (몇몇 사람들은, 세부사항들을 아주 쉽게 배울 수 있다고 주장하지만, 그 부분은 확신할 수 없다.)
컨디션은 자바와 유사한 언어들에서의 익셉션(exceptions)과 거의 비슷하다. 하지만 컨디션이 더 강력한데, 이는 예를 들어, 다른 일은 하지 않고 컨디션이 일어났던 부분에서부터 수행을 재개하도록 컨디션 핸들러가 Lisp 런타임 시스템을 유도하는 것도 가능하기 때문이다. 익셉션 핸들러에 익숙해 있다면 이는 처음엔 이상하게 보일 수도 있다: 익셥션은 항상 거기에서 뭔가 문제가 발생했다는 의미가 아닌가? 음, CL에서는 문제가 없어도 컨디션이 발생될 수 있다. 예를 들어, 멀티 스레드 코드가 동기화 되어야 한다던가, 다른 코드들이 어떤 특정 이벤트의 발생을 보고받아야 한다던가, 혹은 당신이 유용할 것이라고 생각되는 다른 어떤 것이건 간에. 또한, 진짜 문제를 알리는 컨디션의 경우, 수행 중에 사용자에 의해서건 자동으로건 문제를 수정할 수도 있다.
즉, 대부분의 객체지향 언어의 모델들이 CLOS의 특별한 경우인 것과 똑같이, 자바의 익셉션 처리는 CL의 더 강력한 컨디션 시스템의 특별한 경우이다.
CL의 컨디션 처리에 대한 훌륭한 개요와, 몇가지 재미있는 역사적 사실들에 대한 정보는 다음 논문에서 찾을 수 있다.
몇가지 주의:
CL은, 함수에서의 non-local exit를 위한 catch 와 throw 구문을 제공한다. 이들은 자바에서의 try-catch-finally 삼총사(triple)와 유사한 것이 아니다. 대신에, handler-case나 handler-bind 등등을 써서, try-catch 블럭을 나타내고, unwind-protect로 try-finnally 블럭을 나타낸다. 자세한 사항은 위의 논문이나 스펙을 참조하라.
나는 이미 Paul Graham의 책 "On Lisp"을 매크로 프로그래밍과 관련해서 언급했다. 이 책은 또한 많은 다른 고급 프로그래밍 기술들도 설명하는데, 예를 들면 유틸리티 함수의 생성, 고수준 함수, 데이터베이스 접근, 스킴의 계산과정(continuations)을 흉내내기, 멀티 프로세스, 비결정성, 파싱, 로직 프로그래밍, 객체 지향 등등이다. (하지만, 책의 많은 부분은 매크로를 다루고 있다.)
편의를 위해 다시 링크해 둔다.
자바는 괜찮은 모듈 시스템을 가지고 있는데, 그것을 이용하여 클래스들을 한데 묶어 패키지로 만들 수 있고, 그것들은 서로간에 어느정도의 보호를 가능하게 한다. 또한, 패키지 구조는 대응되는 파일과 디렉토리 구조에 반영되고, 이는 zip이나 jar 파일 내에 숨겨지게 된다. 이는 단순하지만 세밀하게 클래스 경로를 다룰 수 있게 하는데, 클래스 경로는 필요에 따라 자바 런타임에 의해 로드될 수 있는 클래스들을 찾을 수 있는 곳을 정의한다.
CL 역시, 정의(definitions)들(물론 클래스의 정의도 포함하여)의 묶음을 다룰 수 있는 패키지 시스템을 제공한다. 그러나, 여기에는 파일과 디렉토리 구조와의 대응 같은 것은 없다. CL의 패키지 시스템은 단지, 정의들이 Lisp 환경 내에서 런타임시에 어떻게 관리(arranged)되는가를 다룬다.
사실, CL의 패키지 시스템은 그보다 더 강력한데, 이는 일반적으로 말해 패키지 시스템이 심볼(symbols)들을 패키지화 하도록 허용하기 때문이다. CL에서의 정의들은 항상 심볼들에 의해 접근되므로, 정의들을 묶을 수 있는 기능은, 단지 더 일반적인 패키지 개념의 특수한 예이다. 다음은 패키지로 달리 무엇을 할 수 있는지에 대한 좋은 예제로, Kent M. Pitman에 의해 comp.lang.lisp 에 포스팅 되었던 것이다. (아래 예를 포함하는 완전한 메시지를 보기 위해서는 http://makeashorterlink.com/?E3B823F91 를 참조하라.)
(in-package "FLOWER")
(setf (get 'rose 'color) 'red)
(setf (get 'daisy 'color) 'white)
(defun colors-among (things)
(loop for thing in things
when (get thing 'color)
collect thing))
(in-package "PEOPLE")
(setf (get 'rose 'color) 'white)
(setf (get 'daisy 'color) 'black)
(defun colors-among (things)
(loop for thing in things
when (get thing 'color)
collect thing))
(in-package "OTHER")
(flower:colors-among '(flower:rose people:rose flower:daisy))
=> (FLOWER:RED FLOWER:WHITE)
(flower:colors-among '(flower:rose people:daisy people:rose))
=> (FLOWER:RED)
(people:colors-among '(flower:rose people:rose flower:daisy))
=> (PEOPLE:WHITE)
(people:colors-among '(flower:rose people:daisy people:rose))
=> (PEOPLE:BLACK PEOPLE:WHITE)
즉, CL의 패키지 시스템은 심볼들의 묶음을 다루고, 그 부수효과(side effect)로 정의들의 묶음도 허용하지만, 시스템의 부분들의 검색이나 로딩은 전혀 다루지 않는다. CL 용어로 말하면, 후자들은 "시스템 생성 (system contruction)" 이라는 업무로 불린다.
따라서 이런 측면에서 혼동하지 말기 바란다. 자바의 패키지와 CL의 패키지는 우연히 같은 이름을 가지지만, (약간의 겹침은 있으나) 다른 용도로 사용된다.
CL은 단지, 시스템 생성을 위한 기본적인 지원들만을 정의한다. ("load"나 "require" 등의 함수들이 그것이다. - CLtL2와 ANSI 스펙을 보라.) 이 이슈에 대한 추가적인 정보는 다음 섹션에서 다뤄질 것이다.
ANSI CL 표준화는 1994년에 완료되어 1995년에 발표되었다. 그때는 자바가 거의 나오려는 참이었지만, 아직 공표되지는 않은 시점이었다. 그때는 또한 인터넷의 상업적 부상 이전이었다. 과거 7년 동안, 프로그래밍에 대한 지원은 명백히 여러 방향으로 진화해 왔다. 불행히도 그동안 CL의 "공식적인" 표준화는 계속되지 않았다. 많은 유행하는(fashionable) 프로그래밍 언어에서 요즘은 당연한 것으로 여겨지고 있는 여러가지 것들이, ANSI CL에는 포함되어 있지 않다. 모듈 기능("시스템 생성"), 유니코드 지원, 플랫폼 독립적인 GUI 라이브러리, 소켓과 TCP/IP, XML, 웹서비스 등등 - 당신이 좋아하는 것을 여기 추가해도 좋다 - 이 포함된다.
그러나 Lisp의 세계는 그동안 가만 있지는 않았다. ANSI CL 표준에 명세되어 있지 않다는 것이, 그것이 존재하지 않는다는 뜻은 아니다.
두가지 널리 쓰이는 시스템 생성 기능(facilities)은 ASDF (http://www.cliki.net/asdf)와 MK-DEFSYSTEM (http://www.cliki.net/mk-defsystem) 이다. 또한 몇몇 CL 구현들은 자신의 시스템 생성 지원 기능을 가지고 있따. Allegro Common Lisp, LispWorks, CLISP은 유니코드 지원을 제공한다. CLIM은 플랫폼 독립적인 GUI 라이브러리로 Franz, Xanalys, Digitool 등의 회사에서 지원된다. 대부분의 쓸만한 CL 구현들은 소켓과 더 진보된 네트웍 기능을 지원한다. CL-XML은 XML을 다룰 수 있는 라이브러리이다.
따라서 기본적으로, 만일 뭔가 라이브러리가 필요하다면 구글(Google)을 이용하여 쓸만한 라이브러리를 찾아낼 수 있을 것이다. 또한 아래의 링크들도 참고하라.
아래는 유용하고 재미있는 링크들이다. 전부는 아니지만 몇몇은 위의 텍스트에서 이미 언급되었다.
Many thanks (in alphabetical order) to Tom Arbuckle (http://www.cs.uni-bonn.de/~arbuckle/), Joe Bergin (http://csis.pace.edu/~bergin/) and Richard Gabriel (http://www.dreamsongs.com/) for providing lots of useful feedback on the draft version.
Further thanks for even more feedback go to Paolo Amoroso, Marco Antoniotti, Tim Bradshaw, Christopher Browne, Thomas F. Burdick, Wolfhard Buß, Bill Clementson, Matthew Danish, Biep Durieux, Knut Aril Erstad, Frode Vatvedt Fjeld, John Foderaro, Paul Foley, Erann Gatt, Martti Halminen, Bruce Hoult, Arthur Lemmens, Barry Margolin, Nicolas Neuss, Duane Rettig, Dorai Sitaram, Aleksandr Skobelev, Thomas Stegen, Gene Michael Stover, Raymond Toy, Sashank Varma and Espen Vestre. (Many of them are active participators in comp.lang.lisp.)