레이블이 Common Lisp인 게시물을 표시합니다. 모든 게시물 표시
레이블이 Common Lisp인 게시물을 표시합니다. 모든 게시물 표시

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 가이드를 참고하기 바란다.

2008년 5월 6일 화요일

Weblocks 웹 프레임워크 가이드

4월 한 달 동안은 논산 훈련소에 다녀오느라 공부도, 포스팅도 전혀 하지 못했다. 덕분에 Weblocks 코드를 보면서 파악했던 것들도 많이 까먹은 것 같다. Weblocks는 현재 리습 유저로서 선택할 수 있는 거의 유일한 웹 프레임워크라고 생각된다. 함부로 말할 수는 없겠지만 아이디어도 깔끔하고 여러모로 Rails보다도 나은 면이 많은 것 같다. 다만 여타 오픈 소스 소프트웨어와 마찬가지로 처음 접하는 사람에게 도움을 줄 만한 문서가 부족하다. 도움이 될 만한 문서가 있다고 해도 토론 그룹에 흩어져 있는 경우가 많아서 찾기도 쉽지 않다. 직접 자세하게 가이드를 써 보면 좋겠지만 아직 그럴 내공도 안 되고, 그래서 Weblocks를 이해하는데 도움이 될 만한 글들을 한데 링크해보는 것으로 그치려고 한다.

공식 매뉴얼
view에 관한 introduction

언제나 그렇지만 뭔가를 익히기 위한 제일 좋은 방법은 예를 보는 것이다.
hello world example
A simple blog example
이외에도 다운받은 코드의 cl-weblocks/examples/ 디렉토리 아래에 있는 예들을 살펴보는 것도 꽤나 도움이 된다.

파일 업로드에 관한 스레드
Lisp for the Web - Weblocks와 직접 관련있는 글은 아니지만 Weblocks가 hunchentoot와 cl-who 라이브러리에 바탕을 두고 있기 때문에 저 둘에 관련된 아티클도 도움이 될 거라 생각한다.
Weblocks에서 새로운 타입을 정의하는 것에 관한 아티클

2008년 3월 27일 목요일

적응형 소프트웨어로서의 웹 어플리케이션

다양한 브라우저에서 "동일하게 보이고 동일하게 동작하는" 웹 어플리케이션을 만들기란 쉽지 않다. 브라우저라는 환경이 너무나 다양한 탓이다. 다양한 환경을 다루는 솔루션은 단일한 환경을 다루는 솔루션에 비해 복잡해지기 마련이다.

같은 이유로 적응형 소프트웨어를 만드는 것도 어려운 일이다. 사실, "다양한 환경에서 제대로 기능하는 것"이 적응형 소프트웨어의 목표라고 할 때, 웹 어플리케이션은 브라우저라는 다양한 환경에서 제대로 동작하려고 애쓰는 적응형 소프트웨어라고도 할 수 있다.

여러 개의 솔루션을 종합해서 하나로 만든 솔루션은 복잡하기 때문에 유지보수가 어렵다. 다음 예를 보자.

#sidebar {
padding: 10px;
border: 5px solid black;
width: 230px; /* for Internet Explorer */
voice-family: “\“3\””;
voice-family: inherit;
width: 200px; /* real value */
}

html>body #sidebar {
width: 200px;
} /* for Opera */

이 코드는 인터넷 익스플로러와 오페라, 기타 브라우저에서 모두 똑같은 모양의 웹 페이지를 보여주기 위한 CSS 코드이다. 인터넷 익스플로러가 너비 값을 해석하는 방식의 차이 때문에 인터넷 익스플로러를 위한 너비값을 먼저 지정한 후, voice-family 속성을 사용해 인터넷 익스플로러가 선언이 끝난 것처럼 인식하게 하고, 그 후에 다른 브라우저를 위한 너비 값을 선언한 것이다. 하지만 오페라 브라우저도 인터넷 익스플로러처럼 voice-family 속성을 만나면 해석을 멈추기 때문에 아래 쪽에 오페라 브라우저만을 위해 추가적인 선언을 해 주었다. 이 코드는 서로 다른 브라우저 환경에서 동일한 모양을 보여준다는 목표를 달성하기 위해 일종의 CSS 트릭을 이용하고 있다. 이런 방식은 코드를 복잡하게 만들 뿐 아니라 브라우저들의 버전이 바뀌어 CSS를 해석하는 규칙이 변한다면 더 이상 유효하지 않게 된다. 하지만 CSS 기법을 담고 있는 많은 책들이 서로 다른 브라우저 문제를 해결하기 위해 이와 비슷한 트릭들에 의존하고 있다.

위 코드를 주석 없이 본다면 사실상 어떤 부분이 인터넷 익스플로러를 위한 부분이고 어떤 부분이 나머지 브라우저를 위한 부분인지, 맨 아래 오페라를 위해 삽입한 코드는 왜 들어가 있는건지, 알기가 어렵다. 하지만 각각의 CSS 코드를 분리해서 상황에 맞게 로드할 수 있다면 다음과 같이 될 것이다.

/* Internet Explorer */
#sidebar {
width: 230px;
padding: 10px;
border: 5px;
}

/* the others */
#sidebar {
width: 200px;
padding: 10px;
border: 5px;
}

이 편이 코드를 읽기는 훨씬 쉽다. 따라서 환경에 대응하는 각각의 솔루션을 분리하는 것이 복잡도를 줄이는 방법이다. 하지만 S라는 솔루션을 Sa, Sb, ... Sz 으로 분리했을 경우, 요구사항이 변하거나 다른 해결 방법을 사용하고자 하여 Sa가 Sa'으로 변경되면 Sb, ... Sz 도 같이 변경해 주어야 한다는 번거로움이 생긴다.

따라서 이상적인 방법은 Sa와 나머지 솔루션 사이의 차이를 Vab, Vac, ... Vaz로 정의한 다음, Sa만을 변경해 나가고 나머지 솔루션은 Sa에 Vab ... Vaz를 적용해 자동으로 생성하는 것이다.

이와 같은 생각을 웹 어플리케이션에 적용해보자. 하나의 브라우저에 대해서만 CSS나 자바스크립트 코드를 작성한 후 브라우저 사이의 차이를 정의한 룰을 적용해서 다른 브라우저들에 적합한 CSS와 자바스크립트 코드를 생성해내는 것이 가능할 것이다. 일단 각 브라우저에 적합한 코드들을 만들어 냈다면, 리퀘스트의 정보를 살펴보고 해당 브라우저에 맞는 코드를 로드하게 만드는 것은 쉬운 일이다.

솔루션 사이의 차이를 어떻게 기술할 수 있을까? 필요한 것은 코드에서 "관심의 대상이 되는 부분"을 정의하고 그 부분에 "규칙에 의한 변화"를 가해서 다른 솔루션을 만들어내는 것이다. AOP의 pointcut과 advice가 의도는 다를지언정 이와 비슷한 역할을 한다고 할 수 있다. 따라서 솔루션 사이의 variation을 정의하기 위해, pointcut과 advice 같은 형식을 이용할 수 있다.

이와 같은 생각을 variation-applier라는 도구로 구현해보았다. 이 도구는 CSS 파일을 대상으로 한다. 사용 방법은 기본 솔루션에 해당하는 디렉토리와, variation 룰들(IE.var opera.var 등등)이 들어 있는 디렉토리, 솔루션들이 생성될 디렉토리를 지정하고 apply를 누르면, variation 룰 당 하나씩 새로운 솔루션이 생성된다.


variation rule은 pointcut과 advice의 개념을 사용했지만 최대한 CSS 문법과 비슷하게 기술하도록 하였다. 사용 예는 다음과 같다.

/* base file */
#sidebar
{ width: 200px; margin: 0;}

case 1
/* var file */
#sidebar
{ width: ((calculate + 30px)); }
=>
/* result file */
#sidebar
{ width: 230px; margin: 0; }

case 2
/* var file */
#sidebar
{ width: ((before border 5px) (calculate * 1.1 + 10) (after padding 10px)); }
=>
/* result file */
#sidebar
{ border: 5px; width: 230px; padding: 10px; margin: 0;}

case 3
/* var file */
global
{ width: ((around border 0)) margin: ((delete)) }
=>
/* result file */
#sidebar
{ border: 0; }

기본적으로 selector가 같을 때만 variation 규칙이 적용되지만 variation의 selector를 global로 선언한 경우에는 selector에 관계없이 모두 적용된다.

variation-applier는 Common Lisp으로 작성되었으며 weblocks 웹 프레임워크 위에서 실행된다. 실행을 위해서는 Common Lisp과 weblocks를 설치하고, 소스 코드의 build-and-test.lisp 파일을 SLIME으로 로드(ctrl+c ctrl+l)한 후, SLIME에서 (variation-applier:start-variation-applier)를 치면 된다. 브라우저의 주소창에 localhost:8080 을 쳐 넣으면 입력 화면이 나타나는 것을 볼 수 있다.

Blogger에는 파일을 첨부할 수가 없기에 티스토리 블로그에 소스 코드를 올려놓았다.
소스 코드 다운로드

2008년 3월 22일 토요일

프로젝트 만들기

매번 패키지 설정을 하는 것이 귀찮아서 자동으로 폴더와 파일을 생성하는 함수를 짰다.

(make-project 'hello)
=>
./hello/
./hello/src/
./hello/test/
./hello/build-and-test.lisp
./hello/hello.asd
./hello/hello-test.asd
./hello/src/hello.lisp
./hello/test/hello-test.lisp

적당한 테스트와 소스 코드를 추가한 후 build-and-test.lisp 파일을 SLIME으로 로드(ctrl+c ctrl+l)하면 빌드 및 테스트가 이루어지고 결과가 표시된다.

아래 코드를 보면 짤린 것처럼 보이지만 긁어서 붙여보면 제대로 보인다; 후.. 블로거에서 코드 들여쓰기를 제대로 보이게 하려면 pre 태그를 쓰는 수밖에 없는데 이마저도 코드가 길어지면 짤려 보이니, 참 난감하다. 누구 블로거에서 코드 제대로 보이게 하는 방법 아시는 분 메일로 알려주시면 정말 감사하겠습니다. ^^;

(defun make-project (project-name)
(let ((name-string (string-downcase (symbol-name project-name))))
(ensure-directories-exist (make-pathname :directory (list :relative name-string)))
(ensure-directories-exist (make-pathname :directory (list :realtive name-string "src")))
(ensure-directories-exist (make-pathname :directory (list :relative name-string "test")))
(with-open-file (build-and-test (make-pathname :directory (list :relative name-string)
:name "build-and-test"
:type "lisp")
:direction :output :if-exists nil)
(format build-and-test
"(push (make-pathname :directory (pathname-directory *load-truename*)) asdf:*central-registry*)~%(require '~A-test)~%(~A-test:~A-test)~%(format t ~A)~%(quit)"
name-string name-string name-string "\"~%\""))
(with-open-file (asd (make-pathname :directory (list :relative name-string)
:name name-string
:type "asd")
:direction :output :if-exists nil)
(format asd
";;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10 -*-~%~%(defpackage #:~A-asd~% (:use :cl :asdf))~%~%(in-package :~A-asd)~%~%(defsystem ~A~% :name \"~A\"~% :depends-on ()~% :components ((:module src :components ((:file \"~A\")))))"
name-string name-string name-string name-string name-string))
(with-open-file (test-asd (make-pathname :directory (list :relative name-string)
:name (concatenate 'string name-string "-test")
:type "asd")
:direction :output :if-exists nil)
(format test-asd
";;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10 -*-~%~%(defpackage #:~A-test-asd~% (:use :cl :asdf))~%~%(in-package :~A-test-asd)~%~%(defsystem ~A-test~% :name \"~A-test\"~% :depends-on (\"~A\" :lisp-unit)~% :components ((:module test :components ((:file \"~A-test\")))))"
name-string name-string name-string name-string name-string name-string))
(with-open-file (main (make-pathname :directory (list :relative name-string "src")
:name name-string
:type "lisp")
:direction :output :if-exists nil)
(format main
"(defpackage #:~A~% (:use :cl))"
name-string))
(with-open-file (test-main (make-pathname :directory (list :relative name-string "test")
:name (concatenate 'string name-string "-test")
:type "lisp")
:direction :output :if-exists nil)
(format test-main
"(defpackage #:~A-test~% (:use :cl :~A :lisp-unit)~% (:export #:~A-test))~%~%(in-package :~A-test)~%~%(defun ~A-test ()~% (run-all-tests :~A-test))"
name-string name-string name-string name-string name-string name-string))))

2008년 3월 10일 월요일

Common Lisp 가이드

사실 리습을 처음 공부하려면 막막하다. 어떤 책으로 시작해야 할지, 수 많은 구현 중에 어떤 구현을 사용해야 할지, 쓸만한 라이브러리나 웹 프레임워크는 어떤 게 있는지.. 알기가 어렵다. 국내에 도움을 얻을 만한 책도, 활발한 커뮤니티도 없다. 그래서 내 경험을 바탕으로, 매우 주관적인 가이드를 써 보고자 한다.

1. 구현(Implementation)
리습은 구현이 여러 개다. 언어 자체를 규정하는 Common Lisp ANSI specification이 있고, 그 specification에 맞게 누구나 커먼 리습을 구현할 수 있다. 많은 구현 중에 SBCL을 사용할 것을 추천한다. 오픈 소스이고, 빠르고, 멀티 스레드를 지원한다. 오픈 소스 중 그 다음으로 추천할만한 구현은 OpenMCL(현재는 Clozure CL로 이름을 바꿨다)정도이다.

2. 설치
리눅스, 맥, 윈도우즈 사용자인데 간편한 설치를 원한다면:
LispBox를 다운받아 설치하는 것이 간편한 방법이다. 윈도우즈 사용자일 경우 Clisp 버전을, 리눅스나 맥 사용자일 경우는 SBCL 버전을 다운받는 것을 추천한다. Clisp은 멀티 스레드를 지원하지 않기 때문에 웹 개발 같은 데 사용하기는 어렵지만, 학습 용도로는 아무 문제가 없다.

맥, 윈도우즈 사용자인데 IDE를 사용해 보고 싶다면:
Lispworks Personal Edition이라는 상용 IDE를 몇 가지 제약과 함께 무료로 사용할 수 있다.

맥 사용자인데 간편한 설치와 좀 더 맥 친화적인 환경을 원한다면:
ReadyLisp이 제격이다.

리눅스나 맥 사용자인데 일일이 직접 설치하기를 원한다면:
리습 작업환경은 사실 Emacs + 리습 구현 + SLIME 이라는 세 가지를 설치한 후 연동해 주는 것이다. Emacs와 리습 구현 자체는 apt-get이나 MacPorts를 통해 쉽게 설치할 수 있을 것이고, SLIME은 내려받아 압축을 푼 뒤 별도의 설치 없이 연동만 하면 된다. 설치방법은 리눅스나 맥이나 거의 동일하다. 다음 글들을 참조하면 된다.
http://onlisp.blogspot.com/2007/12/common-lisp-emacs_12.html
http://onlisp.blogspot.com/2008/01/sbcl-thread.html

3. 에디터
Emacs. 사실 vi나 이클립스를 사용하는 방법도 존재하긴 하지만, 리습의 에디터로는 Emacs가 거의 절대적이라고 생각해도 무방하다.

4. 책
속성으로 아주 기본적인 내용만 빠르게 흝고 싶다면:
http://onlisp.blogspot.com/2007/12/2-ansi-common-lisp.html
http://www.cs.gmu.edu/~sean/lisp/LispTutorial.html
http://psg.com/~dlamkins/sl/chapter03.html

사실 위의 내용만 봐서는 리습을 다른 언어와 차별화하는 특성이 어떤 것인지 알기 어렵다. 리습을 제대로 공부해 보고 싶다면 다음의 책들을 추천한다:

ANSI Common Lisp - Paul Graham
간명하고 재미있게 씌어진 책이다. 다만 온라인에 공개되어 있지 않다는 단점이 있다. 구할 수만 있다면 시작하기에 제일 좋은 책이긴 하다. 일부 챕터의 번역이 인터넷에 올라와 있다.

Chapter 1~4
http://xeraph.com/ => Article/프로그래밍 언어

Chapter 2, 10
http://onlisp.blogspot.com/2007/12/2-ansi-common-lisp.html
http://onlisp.blogspot.com/2007/12/10-ansi-common-lisp.html

Practical Common Lisp
매우 친절하고 자세하게 기술되어 있는 책이다. 온라인에 모두 공개되어 있다는 점도 좋다. 현 시점에서는 가장 추천할만한 책이 아닌가 생각된다. 다만, 저자가 뭐든지 늘여쓰는 경향이 있고, 책 분량이 꽤 되기 때문에 보다가 포기할 가능성도 있다.

Successful Lisp
역시 온라인에서 모두 볼 수 있다. 내용이 그리 많지 않아서 금방금방 볼 수 있고, 유용한 팁들도 꽤 있다.

On Lisp
리습이 가진 최고의 장점이라고 할 수 있는 매크로의 모든 것을 다룬 책이다. 링크한 홈페이지에서 무료로 다운받을 수 있다. 리습에 관한 기본적인 내용을 어느 정도 익혔다고 생각될 때 보는 게 좋다.

5. 커뮤니티
공부하다가 모르는 것에 대해 질문을 해야 할 필요가 있다면 구글 그룹이 좋다. Planet Lisp이라는 곳에서 리습에 관련된 글과 뉴스들을 볼 수 있다.

6. 라이브러리
대부분의 리습 라이브러리들은 Cliki라는 곳에서 찾아볼 수 있다. 다음 라이브러리들을 추천한다.
라이브러리들의 쉬운 설치를 위해: asdf
C와 C++ 라이브러리들을 가져다 쓰려면: cffi
테스팅 프레임워크: lisp-unit
웹 프레임워크: weblocks
웹 서버: hunchentoot
Database: CLSQL
정규식: cl-ppcre

7. 커먼 리습 레퍼런스
언어 자체에 대한 레퍼런스를 보고자 한다면 Common Lisp HyperSpec이 좋다(Emacs에서 slime이 로드된 상태일 때 코드 위에서 ctrl+c ctrl+d h 를 누르면 바로 해당 HyperSpec 페이지로 이동한다). 또는 CLTL2를 참고해도 좋다.

유용한 SLIME 명령어들

사실 코드를 읽다가 repl로 돌아가야 할 경우가 많은데, 그럴 때마다 ctrl+x b로 버퍼를 전환하는 건 여간 귀찮은 일이 아니다. 그럴 경우에 slime repl로 돌아가려면:
ctrl+c ctrl+z

slime을 종료하고 재시동 해야 할 때가 있다. slime 종료는:
, quit

lisp 코드를 읽다가 특정 리습 심볼에 대한 common lisp hyperspec을 보고 싶다면:
ctrl+c ctrl+d h

프로그램을 읽는데, 함수나 변수의 정의된 부분을 살펴보고 싶다면:
meta+.
(meta는 보통 alt. 단 해당 정의부분이 탑레벨에 로드되어 있어야 한다. 예를 들어 define-test 라는 매크로의 정의가 lisp-unit 이라는 라이브러리에 되어 있다면 해당 매크로의 정의를 보기 위해서는 (asdf:operate asdf:load-op 'lisp-unit)과 같이 해당 라이브러리가 로드되어 있어야 한다.)

정의부를 살펴보고 다시 원래 코드로 되돌아가려면:
meta+,
(meta+. 의 반대라고 할 수 있다.)

함수 하나를 컴파일하려면:
커서가 함수 위에 있을 때 ctrl+c ctrl+c

파일을 로드하려면:
로드하려는 파일의 버퍼에서 ctrl+c ctrl+l

파일을 컴파일하고 로드하려면:
컴파일하려는 파일의 버퍼에서 ctrl+c ctrl+k

repl에서 약간의 오타 때문에 거의 똑같은 표현식을 다시 치는 것은 귀찮은 일이다. 입력했던 표현식들을 거꾸로 탐색하려면:
meta+p
반대는:
meta+n

슬라임 쪽 버퍼에 있을 때는 tab만 눌러도 자동완성이 되지만 코드 쪽 버퍼에 있을 때는 meta+tab 이 자동완성.

함수의 caller들을 보고 싶다면:
ctrl+c <
함수의 callee들을 보고 싶다면:
ctrl+c >

슬라임 명령어는 아니고 Emacs 명령어지만 적어본다.
Undo:
ctrl+_
Redo:
ctrl+x z

2008년 2월 5일 화요일

Buildbot과 Subversion으로 빌드와 테스트 자동화하기 2 - Lisp 프로그램의 빌드와 테스트

쉘에서 명령어 하나로 빌드와 테스트를 다 할 수 있다면 Buildbot을 이용해서 빌드와 테스트를 자동화하는 것은 매우 쉬울 것이다. 내 경우에는 build-and-test.lisp 파일에 빌드와 테스트 과정을 저장하고 쉘에서 다음과 같은 명령어로 빌드와 테스트를 수행하였다.

> sbcl --load build-and-test.lisp

build-and-test 파일의 내용은 대체로 다음과 같이 구성될 수 있을 것이다.

; 이 파일이 로드될 시점의 디렉토리를 asdf:*central-registry*에 동적으로 추가
(push (make-pathname :directory (pathname-directory *load-truename*))
asdf:*central-registry*)
; 어플리케이션과 테스트를 빌드
(require 'application)
(require 'application-test)
; 테스트를 수행
(application-test:run-all-tests)
; Buildbot이 빌드와 테스트가 성공한 것으로 인식하도록 sbcl을 정상 종료
(quit)

사실 asdf를 이용해서 시스템을 구성하고 컴파일하는 것에 대해서는 설명이 조금 길어질 수 있다. 하지만 언제나 예를 보는 것이 가장 쉽기 때문에 내 예를 보이도록 하겠다. SBCLlisp-unit을 사용한 예제이다.

내 디렉토리 구조가 다음과 같다고 할 때,

paip> find . -not -iname "*.fasl" -and -not -iname "*~"
.
./build-and-test.lisp
./paip-test.asd
./paip.asd
./src
./src/chapter-1.lisp
./src/chapter-2.lisp
./src/paip.lisp
./test
./test/chapter-1.lisp
./test/chapter-2.lisp
./test/paip-test.lisp

각각의 파일 내용은 다음과 같다.

paip.asd

;;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10 -*-
(defpackage #:paip-asd
(:use :cl :asdf))

(in-package :paip-asd)

(defsystem paip
:name "paip"
:components ((:module src
:components ((:file "paip")
(:file "chapter-1"
:depends-on ("paip"))
(:file "chapter-2"
:depends-on ("paip"))))))

paip-test.asd

;;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10 -*-
(defpackage #:paip-test-asd
(:use :cl :asdf))

(in-package :paip-test-asd)

(defsystem paip-test
:name "paip-test"
:depends-on ("paip" :lisp-unit)
:components ((:module test
:components ((:file "paip-test")
(:file "chapter-1"
:depends-on ("paip-test"))
(:file "chapter-2"
:depends-on ("paip-test"))))))

src/paip.lisp

(defpackage #:paip
(:use :cl)
(:documentation
"Exercises of Paradigms of Artificial Intelligence Programming"))

src/chapter-1.lisp

(in-package :paip)

(export '(power count-atoms))

; 1.2
(defun power (base exp)
(if (= exp 0)
1
(do ((result 1 (* result base))
(exp exp (- exp 1)))
((= exp 0) result))))

; 1.3
(defun count-atoms (expression)
(if (null expression)
0
(if (atom expression)
1
(+ (count-atoms (first expression)) (count-atoms (rest expression))))))

src/chapter-2.lisp

(in-package :paip)

(export '(rule-lhs rule-rhs rewrites))

(defun rule-lhs (rule)
"The left-hand side of a rule."
(first rule))

(defun rule-rhs (rule)
"The right-hand side of a rule."
(rest (rest rule)))

(defun rewrites (category grammar)
"Return a list of the possible rewrites for this category."
(rule-rhs (assoc category grammar)))

test/paip-test.lisp

(defpackage #:paip-test
(:use :cl :paip :lisp-unit)
(:export #:paip-test))

(in-package :paip-test)

(defun paip-test ()
(run-all-tests :paip-test))

test/chapter-1.lisp

(in-package :paip-test)

; 1.2
(define-test power
(assert-equal 1 (power 2 0))
(assert-equal 4 (power 2 2))
(assert-equal 1024 (power 2 10)))

; 1.3
(define-test count-atoms
(assert-equal 0 (count-atoms nil))
(assert-equal 1 (count-atoms 'a))
(assert-equal 3 (count-atoms '(1 2 3)))
(assert-equal 5 (count-atoms '((1) 2 (3 (4) 5)))))

test/chater-2.lisp

(in-package :paip-test)

(define-test rule-lhs
(assert-equal (rule-lhs '(noun -> man ball woman)) 'noun))

(define-test rule-rhs
(assert-equal (rule-rhs '(noun -> man ball woman)) '(man ball woman)))

(defmacro with-simple-grammar (&body body)
`(let ((simple-grammar '((sentence -> (noun-phrase verb-phrase))
(noun-phrase -> (article noun))
(verb-phrase -> (verb noun-phrase))
(article -> the a)
(noun -> man ball woman table)
(verb -> hit took saw liked))))
,@body))

(define-test rewrites
(with-simple-grammar
(assert-equal '(man ball woman table) (rewrites 'noun simple-grammar))
(assert-equal '((article noun)) (rewrites 'noun-phrase simple-grammar))))

build-and-test.lisp

(push (make-pathname :directory (pathname-directory *load-truename*))
asdf:*central-registry*)
(require 'paip-test)
(paip-test:paip-test)
(format t "~%")
(quit)

몇 가지 주의할 점을 얘기해보면,
1. 파일과 디렉토리 사이의 의존 관계를 :depends-on 으로 명확히 표시해 주어야 한다. 그저 의존 순서에 따라 순차적으로 적게되면 때때로 패키지 에러가 발생할 수 있다.

2. 파일이 로드되는 순간에 현재 디렉토리를 알아내서 asdf:*central-registry*에 동적으로 추가할 수 있어야 한다. 그래야 체크아웃하는 디렉토리가 어디인지에 영향을 받지 않고 빌드를 수행할 수 있다. 위의 예대로 하면 되지만, 동적으로 디렉토리와 패스를 알아내는 것에 관심이 있다면 이 포스트를 참조하면 좋다.

Buildbot과 Subversion으로 빌드와 테스트 자동화하기

글을 두 개로 나눠서 적는 것이 좋겠다. 일단 여기서는 Subversion과 Buildbot을 어떻게 설치하고 사용하는지 설명한다. 두번째 글에서는 Lisp 코드의 컴파일과 테스트를 ASDF와 Buildbot을 이용해서 어떻게 자동화하는지 설명하겠다.

I. 기본 개념

'단위 테스트, 버전 관리, 빌드와 테스트 자동화' 이 세 가지는 꼭 수행해야 하는 실행지침으로 꼽힌다. 이 글에서는 버전 관리를 위해서 Subversion을 사용하고, 빌드와 테스트 자동화를 위해서 Buildbot을 사용한다고 가정한다.

Subversion은 CVS와 같은 버전 관리 툴이고, CVS의 단점을 보완하기 위해서 개발되었다고 한다. 현재는 가장 널리 쓰이고 있지만, darcs라는 툴로 많은 사람이 넘어가고 있는 것 같기도 하다. (같은 일을 할 때 darcs가 Subversion보다 손이 덜 간다고 한다. darcs는 패치 등에 대한 이론적 기반을 가지고 만들어진 같고, 가장 radical한 언어 중 하나라고 평가되는 Haskell로 짜여졌다.)

Buildbot은 Python으로 작성된, 빌드와 테스트를 자동화하기 위한 도구이다. 버전 관리 도구의 저장소를 감시하고 있다가 저장소에 변화가 생기면 코드를 체크아웃해와서 빌드와 테스트를 수행하고, 그 결과를 웹이나 이메일로 보여준다. Buildbot의 구조적인 장점은 build master와 build slave가 분리되어 있어서, 서로 다른 플랫폼에서 수행되는 빌드들을 관리하기가 용이하다는 것이다. build master는 각각의 build slave들이 어떻게 build를 수행하고 그 결과를 보고해야 할지에 대한 모든 정보를 가지고 있다. 실제로 빌드를 수행하는 build slave들은 build master에 접속해서 build master가 지시하는대로 할 뿐이다. build master와 build slave는 서로 다른 머신에 위치해 있어도 상관이 없기 때문에, 예를 들어 서로 다른 빌드를 수행하는 머신 1,2,3을 머신 4에서 총괄하는 것이 가능하다.

II. 설치

환경이 리눅스라면,
sudo apt-get install subversion
sudo apt-get install buildbot

환경이 맥이라면,
MacPorts를 이용해서 설치하면 된다. MacPorts는 DarwinPorts의 새 이름으로 리눅스의 apt-get과 비슷하게 각종 소프트웨어의 설치를 쉽게 해 준다. MacPorts 홈페이지에서 dmg 파일을 받아 설치하면 되는데, 설치 후 port 명령이 매뉴얼대로 되지 않는다면, 경로 설정이 안 되어 있어서 그런 것이다. 홈디렉토리(~)에 .profile 파일을 만들어서 다음과 같이 경로를 추가한다.

export PATH=/opt/local/bin:/opt/local/sbin:$PATH

'port search 프로그램이름'(e.x. port search buildbot)과 같이 하면 port를 통해 설치할 수 있는 프로그램들을 찾아볼 수 있다. 원하는 프로그램 이름을 찾으면 'sudo port install buildbot'과 같이 인스톨하면 된다.

환경이 윈도우라면,
이 경우는 해보질 않아서 모르겠다. 설치는 각각의 홈페이지를 참조해서 어떻게든 알아서;

III. 사용법

Subversion

저장소를 로컬의 /Users/peter/svn-repos 에 만든다고 할 때 각종 명령의 예는 다음과 같다. 로컬이 아니고 웹인 경우에는 'file://'부분을 'http://'로 바꾸면 된다.

새로운 저장소 만들기
svnadmin create /Users/peter/svn-repos

저장소에 프로젝트 만들기(기존 소스 코드를 저장소 안의 한 디렉토리로 임포트하는 경우)
app> svn import -m "application initial import" file:///Users/peter/svn-repos/app
이 경우는 저장소 아래에 app 디렉토리를 만들고 현재 디렉토리 아래의 모든 디렉토리와 파일들을 집어넣은 것이다. 하지만 저장소의 디렉토리를 살펴봐도 app 디렉토리는 눈에 보이지 않는다. 저장소에 저장된 프로젝트의 디렉토리 위치를 보고 싶다면 어떻게 해야 할까?
1. 체크아웃 해 온 작업본 디렉토리에서 svn info 또는,
2. 아무 곳에서나 svnlook tree /Users/peter/svn-repos

저장소로부터 파일 체크아웃하기
svn co file:///Users/peter/svn-repos/app app

작업본 디렉토리의 상태 알아보기
svn status
파일 앞의 M 표시는 그 파일이 변경되었다는 것을, ? 표시는 그 파일이 버전 관리 하에 놓여있지 않다는 것을 나타낸다. ? 표시된 파일을 버전 관리 대상으로 추가하기 위해서는 svn add를 사용

작업본 디렉토리의 변경을 저장소에 저장
app> svn commit -m "message"

리비전 내역 전체를 보려면
app> svn log

작업본 디렉토리의 내용을 최근 저장소의 리비전으로 갱신하려면
svn update

작업본 디렉토리에서 수행한 변경을 체크아웃 해 온 시점으로 되돌리려면
svn revert .

리비전 10과 리비전 11 사이의 변경 내역을 보고 싶다면
app> svn diff -r 10:11

작업본 디렉토리의 리비전이 11인데 리비전 9로 되돌리고 싶다면
app> svn merge -r 11:9

기타 알아두면 좋은 것

바이너리 파일이나, 백업 파일들은 저장소에 저장될 필요가 없다. Subversion이 이런 파일들을 무시하게 만들기 위해서는 ~/.subversion/config 파일의 global-ignores 부분을 수정하면 된다. 예를 들어, *.fasl 파일들과 *~ 파일들을 무시하고 싶다면 다음과 같이 수정하면 된다.

global-ignores = *.fasl *~

그리고 에디터가 필요한 Subversion 명령을 사용하게 될 경우가 있다. 예를 들어, emacs를 사용하고 싶다고 하면, ~/.profile 에 다음과 같이 추가한다.

SVN_EDITOR=/usr/bin/emacs
export SVN_EDITOR


Buildbot

1. build master를 만들자
/Users/peter/buildbot/master/our-master 에 마스터를 만든다고 가정하자.

buildbot create-master /Users/peter/buildbot/master/our-master

2. build master를 설정한다.
our-master 디렉토리에 보면 master.cfg.sample 파일이 있을 것이다. 이 파일을 편집해서 master.cfg 파일로 저장한다. 예를 보는 게 쉬울 것이다.

# 슬레이브 이름과 패스워드를 설정
from buildbot.buildslave import BuildSlave
c['slaves'] = [BuildSlave("slave-name", "slave-passwd")]

# 슬레이브가 접속할 포트 설정
c['slavePortnum'] = 9989

# 저장소의 변화를 어떻게 감지할지 설정한다. Subversion이라면 SVNPoller를 사용
from buildbot.changes.svnpoller import SVNPoller
c['change_source'] = SVNPoller("file:///Users/peter/svn-repos/app")

# treeStableTimer와 빌더 이름을 설정해 주면 된다.
from buildbot.scheduler import Scheduler
c['schedulers'] = []
c['schedulers'].append(Scheduler(name="all", branch=None,
treeStableTimer=2*60,
builderNames=["our-builder"]))

# 빌드 절차를 팩토리로 만드는 부분. 체크아웃->빌드 및 테스트 순서로 이루어져야 할 것이다.
# svnurl 에 빌드를 위해 체크아웃 해 와야 할 저장소의 디렉토리를 넣는다.
# ShellCommand 로 쉘에서 수행할 명령을 지정할 수 있다.
# 이 경우에는 build-and-test.lisp 파일에 빌드와 테스트 방법을 넣어놓고 로드하게 한 것이다.
from buildbot.process import factory
from buildbot.steps import source, shell
f1 = factory.BuildFactory()
f1.addStep(source.SVN(svnurl="file:///Users/peter/svn-repos/app"))
f1.addStep(shell.ShellCommand(command=["sbcl", "--load", "build-and-test.lisp"]))

# 빌더 이름, 빌더가 사용할 슬레이브 이름과 팩토리 이름을 설정해준다.
# builddir은 슬레이브 디렉토리 아래의 어떤 디렉토리에서 빌드를 수행할지를 설정하는 것이다.
# 이 예에서는 슬레이브가 full이라는 디렉토리를 만들어 빌드를 수행할 것이다.
b1 = {'name': "our-builder",
'slavename': "slave-name",
'builddir': "full",
'factory': f1,
}
c['builders'] = [b1]

c['status'] = []

# 웹과 메일로 빌드의 상태를 볼 수 있다.
from buildbot.status import html
c['status'].append(html.WebStatus(http_port=8010))

# sendToInterestedUsers를 False로 설정하면 extraRecipients 로만 메일이 간다고 한다.
# 빌드가 실패할 때만 메일이 가도록 mode='failing'으로 설정할 수도 있다.
# 하지만 무슨 이유에선지 메일은 제대로 보내지질 않는 것 같다.
from buildbot.status import mail
c['status'].append(mail.MailNotifier(fromaddr="buildbot@localhost", extraRecipients=['peter@matrix'], sendToInterestedUsers=False)

c['buildbotURL'] = "http://localhost:8010/"

3. build slave를 만들자
/Users/peter/buildbot/slave/our-slave 에 슬레이브를 만들고, 위에서 한 설정을 사용한다고 가정하자.

buildbot create-slave /Users/peter/buildbot/slave/our-slave localhost:9989 slave-name slave-passwd

4. build master와 build slave를 실행시키자
buildbot start /Users/peter/buildbot/master/our-master
buildbot start /Users/peter/buildbot/slave/our-slave

build master가 master.cfg 파일을 다시 읽어들이게 하려면
buildbot reconfig /Users/peter/buildbot/master/our-master

master와 slave를 멈추려면
buildbot stop /Users/peter/buildbot/slave/our-slave
buildbot stop /Users/peter/buildbot/master/our-master

이제 localhost:8010 을 살펴보자. 모든 것이 제대로 되었다면 BuildSlave를 클릭했을 때 Slave가 연결된 상태라고 나올 것이다. 저장소에 커밋이 일어나면 treeStableTimer 만큼 기다렸다가 체크아웃과 빌드가 수행된다. 쉘에서 수행된 커맨드의 exit code가 0이 아닌 값이거나 아무 stdout이 없이 20분이 지나면 빌드는 실패한 것으로 간주된다. 그 외의 경우는 성공한 것으로 처리되고, 어떤 경우든 stdout이 로그로 남는다. 웹의 Waterfall Display에서 시간대별로 모든 체크아웃과 빌드의 로그를 볼 수 있다.

여기까지 Subversion과 Buildbot을 살펴보았다. 빌드 과정은 빌드해야 하는 프로그램의 언어가 무엇이냐에 따라서 달라질 수 있다. Lisp 프로그램을 빌드하고 테스트하는 것에 대해서는 다음 글에서 다루도록 하겠다.

2008년 1월 9일 수요일

Emacs에서 Common Lisp Hyperspec 보기

리습 코드를 읽다보면 아무래도 모르는 함수나 매크로에 대한 레퍼런스를 찾아볼 필요가 생긴다. Common Lisp Hyperspec이 웹에 있고 HTML 파일을 내려받아 로컬에 저장해 놓고 볼 수도 있지만 인덱스를 일일이 클릭해가며 모르는 심볼을 찾아보는 것은 귀찮은 일이다.

보다 나은 방법은 커서가 심볼 위에 있을 때 ctrl+c ctrl+d h 를 입력하는 것이다(slime이 실행되어 있는 상태여야 한다). 그러면 해당 심볼에 해당하는 Hyperspec 웹 페이지가 뜨게 된다. 하지만 웹 페이지를 띄우는 방법은 느리고, Emacs를 벗어나야 한다는 게 불편하다. 그래서 Emacs 내에서 Hyperspec을 보는 방법을 적어보려고 한다.

Reference:
http://www.foldr.org/~michaelw/log/programming/lisp/dpans-texinfo-edition
http://www.phys.au.dk/~harder/dpans.html

1. Texinfo가 필요하다. 최신 버전을 다운로드 받아서 설치한다. 설치는 내려받은 소스의 압축을 푼 폴더에서 다음과 같이 입력하면 된다.

./configure
make
sudo make install

그런데 소스 폴더에 있는 INSTALL 파일을 읽어보면 위와 같은 일반적인 설치 과정만으로는 texinfo.tex 와 *.tex 파일들은 설치되지 않으니 추가적인 설치 명령이 필요하다고 하는데, 파일에서 말하는 TEXMF라는 게 무엇인지 모르겠다. 일단 위와 같은 일반적인 설치만으로도 목적을 달성하는 데는 별 문제가 없었으니 넘어가자.

2. dpans2texi-1.03.tar.gz를 다운 받아 압축을 푼 후 압축이 풀린 디렉토리에서 다음과 같이 입력한다.

./configure
wget ftp://parcftp.xerox.com/pub/cl/dpANS3/*
wget ftp://parcftp.xerox.com/pub/cl/dpANS3R/*
make
sudo make install

만일 wget이 없다면 설치하도록 하자. 소스 파일을 다운로드해서 압축을 푼 후 디렉토리에서 다음과 같이 입력하면 된다.

./configure
make
sudo make install

3. .emacs 파일(emacs startup file)에 다음과 같이 추가한다.

(require 'info-look)
(info-lookup-add-help
:mode 'lisp-mode
:regexp "[^][()'\" \t\n]+"
:ignore-case t
:doc-spec '(("(ansicl)Symbol Index" nil nil nil)))

이제 리습 코드의 심볼 위에 커서가 있을 때 ctrl+h S 를 입력하고 엔터를 치면 해당 심볼에 대한 레퍼런스가 바로 뜰 것이다.(글의 시작 부분에 링크한 Reference에는 ctrl+h ctrl+i 라고 써 있지만 최근 버전의 Emacs에서 키 바인딩이 바뀌면서 기존 단축키가 먹지 않는다. ctrl+h 대문자S 를 눌러야 한다.) 그리고 ctrl+h i 를 눌러서 info 페이지로 들어가면 Programming 카테고리에 ANSI Common Lisp이 생긴 것을 볼 수 있다.

다른 방법을 소개하고 있는 페이지들도 링크해본다.
http://www.newartisans.com/blog_files/hyperspec.info.for.emacs.php
http://bc.tech.coop/blog/031002.html
http://lemonodor.com/archives/2002/02/living_in_emacs.html

2008년 1월 4일 금요일

맥에서 SBCL이 thread를 지원하도록 만들기

SBCL은 맥에서 experimental threading을 지원한다고 되어있지만, 그런 식으로 발표된지가 1년이 훨씬 넘었기 때문에 더 이상 experimental한 것이 아니라고 보는 사람들이 많다. 어쨋건, 맥에서 SBCL로 웹 프로그래밍을 해보려면 threading support는 필수적이기 때문에 SBCL을 어떤 식으로 인스톨해야 스레드를 지원하도록 만들 수 있는지 알아보도록 하겠다. 요점은 특정한 설정을 주고 소스를 직접 컴파일해야 되는데, 컴파일하려면 리습 구현이 필요하기 때문에 일단 바이너리 버전의 SBCL을 설치한 다음, 소스코드를 컴파일해서 바이너리를 만들고 그 바이너리로 인스톨하면 된다. 구체적인 설치 과정은 다음과 같다.

1. 바이너리 버전의 SBCL을 다운받아 인스톨한다.
바이너리 파일을 다운받아 압축을 푼 폴더에서 sudo sh install.sh

2. SBCL의 소스 코드를 내려받아 압축을 푼다.
예를 들어 압축을 푼 폴더가 /Users/chanwoo/Downloads/sbcl-1.0.13 라고 하자.

3. 내려받은 소스 코드 디렉토리의 최상단(/Users/chanwoo/Downloads/sbcl-1.0.13 폴더)에 customize-target-features.lisp 파일을 다음과 같은 내용으로 만들어 저장한다.

(lambda (features)
(flet ((enable (x)
(pushnew x features))
(disable (x)
(setf features (remove x features))))
;; Threading support, available only on x86/x86-64 Linux, x86 Solaris
;; and x86 Mac OS X (experimental).
(enable :sb-thread)))

4. 소스를 컴파일한다.
sudo sh make.sh

5. 기존에 설치된 SBCL을 지운다.
/usr/local/bin/sbcl 과 /usr/local/lib/sbcl 을 삭제

6. 소스 디렉토리(/Users/chanwoo/Downloads/sbcl-1.0.13)에서 바이너리 파일을 인스톨하듯이 인스톨하면 된다.
sudo sh install.sh

Hunchentoot를 돌려보면 스레드를 지원하는 것을 볼 수 있다! :)

2008년 1월 2일 수요일

ASDF (Another System Definition Facility)

커먼 리습 프로그래밍을 하다보면 홈 폴더가 아닌 다른 폴더에 저장된 .lisp 파일을 로드해 올 수는 없을까 하는 의문이 생기게 된다. load가 파일을 읽어들여오는 path 설정은 도대체 어디서 하는거지? 라고 고민할 수 있다. 결론적으로 말하면 load는 파일을 로드하기 위해서 복수의 디렉토리를 살펴볼 능력이 없다. 하지만 방법이 없는 것은 아니다. asdf를 사용하면 원하는 일을 할 수 있다. SBCL이나 OpenMCL 같은 경우에는 asdf가 기본적으로 포함되어 있기 때문에 별도의 설치가 필요 없다. asdf를 사용할 때 다음과 같이 해주기만 하면 된다.

(require 'asdf)

asdf는 사실 프로그램이 복수의 파일들로 나뉘어서 짜여질 때 파일들 사이의 의존관계를 지켜가며 컴파일하도록 도와주는 C의 make와 비슷한 것이라 보면 된다. 이러한 의존관계는 .asd 파일로 만들어진다. 다음 cow.asd 파일의 예를 보면 tail.lisp 파일은 legs.lisp 파일에 의존적이다.

;;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10 -*-

(defpackage #:cow-asd
(:use :cl :asdf))

(in-package :cow-asd)

(defsystem cow
:name "cow"
:version "0.0.0"
:maintainer "T. God"
:author "Desmon Table"
:licence "BSD sans advertising clause (see file COPYING for details)"
:description "Cow"
:long-description "Lisp implementation of our favorite ruminant"
:components ((:file "tail"
:depends-on ("package" "legs"))
(:file "legs"
:depends-on ("package"))
(:file "head"
:depends-on ("package"))
(:file "package")))

cow.asd 및 tail.lisp, legs.lisp 등의 파일이 ~/work/cow 폴더에 저장되어 있다고 하자. 그러면 이 시스템을 사용하기 위해서는 자신이 사용하는 리습의 스타트업 파일(홈폴더 아래에 만들면 되며 SBCL은 .sbclrc OpenMCL은 openmcl-init.lisp)에 다음과 같이 추가하면 된다.

(require 'asdf)
(push #p"~/work/cow" asdf:*central-registry*)

이렇게 추가한 뒤에는 SBCL에서는 그저 (require 'cow)라고 하는 것만으로 파일들이 차례대로 로드된다. 다른 리습 구현에서는 (asdf:operate 'asdf:load-op 'cow)라고 해야 하지만.

따라서 asdf를 이용하게 되면 홈 폴더가 아닌 곳에 리습 파일을 놓고 로드할 수가 있다. 리습 파일을 원하는 폴더에 위치시키고 그 폴더를 asdf:*central-registry*에 등록시킨 다음, 폴더 안에 있는 .lisp 파일들을 어떤 식으로 로드할 것인지를 .asd 파일로 만들어 역시 같은 폴더 안에 위치시키면 된다. 만일 로드하고자 하는 리습 파일이 lisp-unit.lisp파일 하나뿐이라면 lisp-unit.asd 파일은 다음과 같은 것으로 충분할 것이다.

(defpackage #:lisp-unit-asd
(:use :cl :asdf))

(in-package :lisp-unit-asd)

(defsystem lisp-unit
:name "lisp-unit"
:components ((:file "lisp-unit")))

아무튼, 여기서 다 설명할 수는 없지만, asdf는 현재 Common Lisp에서 시스템을 구성하는 일반적인 방법이다. 그리고 asdf-install은 Common Lisp 라이브러리를 설치하는데 가장 널리 사용되는 도구다. 둘 다 Common Lisp의 교과서들을 끝내고 실제 세상으로 나갈 때 반드시 사용하게 되는 도구라고 할 수 있다.

2008년 1월 1일 화요일

Common Lisp Testing Framework

어떤 프로그래밍 언어를 사용해서 프로그램을 하든 테스팅 프레임워크와 프로파일러는 필요하다. 커먼 리습의 테스팅 프레임워크들을 잘 정리해놓은 글이 있어 링크해본다. Common Lisp Testing Framework

lisp-unit은 사용법이 아주 간단할 뿐더러 문서화도 잘 되어 있다. 하지만 테스트를 조직화 할 수 있는 방법이 마땅치 않아서 간단한 테스트만을 필요로 할 때 적합하다고 한다. FiveAMStefil은 좀 더 스케일이 크고 조직화된 테스트에 적합하다고 한다.

1. lisp-unit 설치

lisp-unit은 원래 파일 하나로 배포되었지만, 2007년 1월 11일자로 asdf-install 가능하게 바뀌었다. 설치하려면 슬라임에서 다음과 같이 한다.

(require 'asdf-install)
(asdf-install:install 'lisp-unit)

2. FiveAM과 Stefil 설치

FiveAM과 Stefil을 설치하기 전에 해야 할 것이 두 가지 있다.

1. 다음과 같이 최신의 slime을 받아서 사용하는 것이 좋다.

cvs -d :pserver:anonymous:anonymous@common-lisp.net:/project/slime/cvsroot co slime

slime-2.0을 사용하면 컴파일이 제대로 되지 않는다.

2. slime이 위치한 디렉토리를 asdf:*central-registry*에 추가한다.
추가하지 않고 컴파일하면, FiveAM과 Stefil 둘 다 swank라는 것을 요구하는 것을 볼 수 있다. swank는 slime의 다른 이름이다. 리습구현의 스타트업 파일에(SBCL의 경우에는 홈 폴더의 .sbclrc 파일) 다음과 같이 slime이 위치한 디렉토리를 추가해주면 된다.

(push #p"/Users/chanwoo/cl-library/slime/" asdf:*central-registry*)

이제 준비가 되었으면 두 테스팅 프레임워크를 설치해보자.

1. FiveAM 설치
FiveAM은 asdf로 설치 가능하다. 다음과 같이 하면 설치 끝.

(require 'asdf-install) ;GPG체크 때문에 디버거로 떨어지면 0번을 눌러서 진행하면 된다.
(asdf:install-install 'fiveam)

2. Stefil 설치
먼저 iterate, alexandria, defclass-star를 설치해야 한다.
(require 'asdf-install)
(asdf-install:install 'iterate)
(asdf-install:install 'alexandria)

defclass-star는 다음과 같이 darcs로 내려받은 후,
darcs get http://common-lisp.net/project/defclass-star/darcs/defclass-star
폴더를 asdf:*central-registry*에 추가하면 설치 끝이다.

세 가지를 다 설치했다면, 이제 Stefil을 다음과 같이 내려받은 후,

darcs get http://common-lisp.net/project/stefil/darcs/stefil

해당 디렉토리를 asdf:*central-registry*에 추가하면 설치 끝.

설치가 끝나고 FiveAM이나 Stefil을 사용하려면 SBCL에서는 단순히,
(require 'fiveam) 또는 (require 'stefil) 로 충분하다.
SBCL외의 다른 구현을 사용한다면,
(asdf:operate 'asdf:load-op 'fiveam) 또는 (asdf:operate 'asdf:load-op 'stefil) 라고 하면 된다.

2007년 12월 30일 일요일

Ready Lisp

Common Lisp을 처음 시작하는 사람에게는 적절한 리습 구현을 골라 설치하고, 이맥스와 슬라임을 연동하고 하는 작업이 꽤나 번거롭게 느껴질 수 있다. 하지만 맥 유저라면 아주 쉬운 길이 있다. 바로 Ready Lisp이다. 보기에도 예쁜 Aquaemacs와 리습 구현 중에서 실행 속도가 가장 빠른 편에 속하는 SBCL, 슬라임을 한꺼번에 묶어놓은 패키지다. 더구나 SBCL은 맥에서는 'experimental multi-thread'만을 지원하는데, 멀티스레드를 지원하게 만들려면, SBCL의 소스 파일을 받아서 직접 컴파일하는 수고를 들여야 한다. 하지만 Ready Lisp은 SBCL이 멀티스레드를 지원하도록 컴파일되어 배포된다. hunchentoot와 weblocks를 돌려본 결과, 멀티스레드가 지원되는 것을 볼 수 있었다.

설치는 그저 dmg 파일을 다운 받아서 더블 클릭한 후 아이콘을 어플리케이션 폴더로 끌어다 놓으면 끝. 단지 종료할 때 ctrl+x ctrl+c로 종료하는 것이 좋다. Aquaemacs는 종료 메뉴를 클릭해서는 잘 종료가 안 되기 때문이다.

그리고 Ready Lisp에는 Common Lisp Hyperspec과 SBCL의 소스 코드가 포함되어 있어서 매우 편리하다. 명령어는 다음과 같다.

ctrl+h f
리습 코드에서 커서가 심볼 위에 있을 때 누르면 해당 심볼에 해당하는 Hyperspec 페이지를 보여준다.
meta(alt)+. 정의
해당 정의의 SBCL 구현 코드를 보여준다.
ctrl+h i
포함되어 있는 여러 가지 문서의 인덱스를 보여준다.

2007년 12월 27일 목요일

8. 언제 매크로를 사용해야 하는가 - On Lisp 번역

8. 언제 매크로를 사용해야 하는가

어떤 것을 함수로 만들어야 할지 매크로로 만들어야 할지를 어떻게 알 수 있을까? 대부분의 경우에는 매크로가 쓰여야 할지 아닐지를 어렵지 않게 판단할 수 있다. 일단 가능하다면 함수를 사용한다: 함수가 할 수 있는 일을 매크로로 하는 것은 바람직하지 않다. 매크로를 사용하는 것이 함수를 사용하는 것에 비해 나은 점이 있을 때 매크로를 사용한다.

그렇다면 언제 매크로를 사용하는 것이 이득인가? 그것이 이 장의 주제이다. 사실 매크로를 사용하는 것이 함수를 사용하는 것에 비해 더 나은 이익을 가져다 주기 때문에 매크로를 사용하는 경우보다는, 어떤 일을 함수로는 할 수 없고 매크로로만 할 수 있기 때문에 매크로를 사용하게 되는 경우가 많다. 섹션 8.1에서 매크로로만 구현될 수 있는 오퍼레이터들이 어떤 것인지를 설명한다. 하지만 때때로 함수를 사용해야 할지 매크로를 사용해야 할지 판단하기 어려운 경우도 있다. 이런 경우에 도움을 주기 위해서 섹션 8.2 에서는 매크로를 사용하는 데 따르는 장단점이 무엇인지 설명한다. 마지막으로 섹션 8.3에서는 매크로를 가지고 어떤 일을 할 수 있는가에 대해서 알아보겠다.


8.1 다른 것들로 해결할 수 없을 때

유사한 형태의 코드가 반복적으로 눈에 띄인다면, 반복이 발생하는 부분을 하나의 서브루틴으로 묶고 서브루틴을 호출하는 것이 일반적인 디자인의 원칙이다. 이 원칙을 리습 프로그램에 적용할 때는 그 서브루틴이 함수가 될 것인지 매크로가 될 것인지를 결정해야 한다.

함수가 아니라 매크로만으로 어떤 일을 할 수 있을 경우에는 고민이 필요없다. 1+ 와 같은 오퍼레이터는 함수와 매크로 둘 다로 작성될 수 있지만:

(defun 1+ (x) (+ 1 x))

(defmacro 1+ (x) `(+ 1 ,x))

섹션 7.3에서 나왔던 while과 같은 경우에는 매크로로만 정의될 수 있다:

(defmacro while (test &body body)
`(do ()
((not ,test))
,@body))

함수로 이와 같은 일을 할 수 있는 방법은 없다. while은 body로 넘어온 표현식을 쪼개서 do 문의 몸체 부분에 넘기는데 이 부분은 test 표현식이 nil을 리턴할 동안만 평가된다. 이런 일을 할 수 있는 함수는 없는데, 함수는 호출되기 전에 모든 인자를 평가해버리기 때문이다.

언제 매크로가 정말로 필요하고, 무엇을 위해 필요할까? 매크로는 함수가 할 수 없는 일을 두 가지 할 수 있다: 매크로는 인자들의 평가 여부를 조절할 수 있고, 매크로가 호출된 문맥 속으로 풀어 헤쳐질 수 있다. 매크로를 필요로 하는 경우는 결국 이 두 가지 중의 하나를 필요로 하는 것이다.

"매크로는 인자를 평가하지 않는다"는 통념은 잘못된 것이다. 정확하게 말하면, 매크로는 그 인자가 평가될지를 조절할 수 있다. 인자가 매크로 확장의 어느 부분에 오느냐에 따라서, 인자는 한 번 또는 여러 번 평가되거나 또는 전혀 평가되지 않을 수 있다. 매크로는 다음의 네 가지 목적을 위해 인자의 평가 여부를 조절할 수 있다.

1. 변형

setf는 인자의 평가가 일어나기 전에 인자를 살펴보고 조작하는 매크로의 일종이다. 커먼 리습의 내장형 함수 중, 값에 접근하기 위해서 쓰이는 함수는 보통 접근한 값을 변화시키기 위한 함수도 쌍으로 가지고 있는 경우가 많다. 리스트의 첫번째 원소에 접근하는 car는 값을 변화시키기 위한 rplaca를 쌍으로 가지고 있고, cdr은 rplacd를 쌍으로 가지고 있는, 그런 식이다. setf는 마치 값에 접근하는 데 사용되는 함수를 값이 저장될 변수처럼 사용할 수 있게 해 주는 매크로이다. 즉, (setf (car x) 'a)를 매크로 확장하면 (progn (rplaca x 'a) 'a)와 같이 될 것이다.

이런 트릭을 위해서는, setf가 첫번째 인자의 표현식을 들여다볼 수 있어야 한다. 위의 경우를 보면, 첫 인자의 표현식이 car로 시작했기 때문에 매크로 확장에는 rplaca가 왔다. cdr로 시작한다면 rplacd가 와야 할 것이다. 즉, setf는 첫번째 인자의 표현식이 car로 시작하는지 cdr로 시작하는지를 들여다 볼 수 있어야 한다. 이와 같이 인자를 살펴보고 그 인자를 변형시켜야 하는 오퍼레이터는 매크로로 작성되어야 한다.


2. 바인딩(binding)

렉시컬 변수(lexical variable - 문자 변수)는 평가된 값이 아니라 문자 자체가 그대로 소스 코드에 나타나야 한다. 예를 들어 setq의 첫 번째 인자로는 렉시컬 변수를 넘겨야 하는데 인자를 모두 평가해 버리는 함수로는 렉시컬 변수를 넘길 방법이 없다. 따라서 setq를 사용해서 무언가를 만들고자 하는 경우에는 매크로로 작성해야 한다. 이와 유사하게, let 속으로 렉시켤 변수를 확장해야 하는 do 같은 것도 역시 매크로로 작성될 수 밖에 없다. 인자들의 렉시컬 바인딩(lexical binding)을 조작해야 하는 경우는 모두 매크로로 작성되어야 한다.


3. 조건부 평가

함수의 모든 인자는 무조건 평가된다. when과 같이, 특정한 조건 하에서만 인자의 일부가 평가되기를 원하는 경우라면, 매크로로만 작성될 수 있다.


4. 반복적 평가(multiple evaluation)

함수의 인자들은 무조건 평가될 뿐 아니라, 정확히 한 번씩만 평가된다. do문에서처럼 어떤 인자를 반복적으로 평가하고 싶을 때는 매크로가 필요하다.


위에서 살펴본 것들 말고도 매크로 확장의 이점을 얻을 수 있는 경우가 몇 가지 더 있다. 매크로 확장이 매크로가 불린 바로 그 지점의 문맥 속에서 일어난다는 것이 중요한데, 매크로 사용의 2/3는 이같은 성질을 이용한 것이다. 이런 경우를 살펴보면:


5. 호출된 곳 주위의 문맥을 사용하는 경우

매크로는 매크로가 불린 문맥에서 바인딩 될 변수를 사용해서 작성될 수 있다. 다음과 같은 매크로는:

(defmacro foo (x)
`(+ ,x y))

foo가 불린 문맥에서 y가 어떻게 바인딩 되어 있느냐에 따라 결과가 달라진다.

이런 식으로 주위 문맥을 사용하는 것은 바람직하지 않은 경우가 많다. functional programming의 원칙은 매크로에도 적용되어야 한다: 매크로에 넘겨지는 매개 변수를 통해 매크로와 상호작용하는 것이 바람직하다. 실수로 일어나는 경우를 제외하면(9장을 보라.), 매크로를 호출한 주위 환경을 이용하는 매크로를 작성하게 되는 경우는 매우 드물다. 이 책에 있는 모든 매크로 중에서, continuation passing 매크로와(20장) ATN 컴파일러의 일부 매크로(23장)만이 이런 식으로 매크로를 호출한 주위 문맥을 이용한다.


6. 새로운 환경을 감싸는 경우

매크로는 그 인자가 새로운 문맥 속에서 평가되도록 할 수 있다. 전형적인 예는 lambda를 사용해서 구현된 매크로인 let이다.(144 페이지를 보라.) 다음과 같은 표현식 내부에서:

(let ((y 2)) (+ x y))

y는 새로운 변수를 참조하게 된다.


7. 함수 호출을 아끼기 위해서

매크로 확장은 매크로가 불려진 곳에서 인라인으로 일어나기 때문에, 이미 컴파일이 끝난 코드는 실행시에 매크로 호출에 따르는 오버헤드가 없다. 런타임에는 매크로가 이미 확장되어 코드로 삽입돼 있기 때문이다.(인라인 함수가 실행시에 함수 호출에 따른 오버헤드가 없는 것과 마찬가지이다.)


위에서 5번과 6번의 경우는, 의도하지 않았는데 실수로 일어나게 된다면, 변수 캡쳐(variable capture)의 문제를 발생시킨다. 의도하지 않은 변수 캡쳐는 매크로를 작성하는 사람이 반드시 피해야 할 실수이다. 변수 캡쳐에 대해서는 9장에서 설명한다.

매크로를 사용하는 7가지 경우가 아니라, 6가지 하고도 반의 경우가 있다고 말하는 것이 더 정확할 것이다. (현실에서는 리습 컴파일러가 inline 옵션을 줘도 무시하는 경우가 있지만) 이상적인 세계에서는 모든 리습 컴파일러가 inline 옵션을 받아들여서, 함수 호출을 아끼기 위해서는, 인라인 함수를 사용하면 되고 매크로를 사용할 필요가 없을 것이기 때문이다.


8.2 매크로와 함수 중 어느 것을?

이전 섹션에서는 명백한 경우들만을 다루었다. 인자들이 평가되기 전에 접근할 필요가 있는 경우는 다른 선택의 여지가 없이 매크로로 작성되어야 한다. 매크로와 함수 둘 다로 작성될 수 있는 경우라면 어떨까? 인자들의 평균을 리턴하는 avg 오퍼레이터의 예를 살펴보자. avg는 다음과 같은 함수로 구현될 수 있다:

(defun avg (&rest args)
(/ (apply #'+ args) (length args)))

하지만 매크로로 작성하는 것이 더 바람직한 경우이기도 하다:

(defmacro avg (&rest args)
'(/ (+ ,@args) ,(length args)))

이 경우에 함수 avg는 매번 불릴 때마다 length를 호출하는데, 이는 불필요한 것이다. 컴파일 타임에 인자들의 값을 알 수는 없지만, 인자들의 개수는 알 수 있기 때문에 length 호출은 컴파일 타임에 이루어지는 것이 좋다. 이와 같이 함수와 매크로 사이에서 선택을 해야 할 때 고려해야 할 점들을 나열하면 다음과 같다:


매크로를 사용했을 때의 장점

1. 연산이 컴파일 타임에 이루어진다.

매크로 호출은 두 단계에 걸쳐 처리된다: 먼저 매크로가 확장된다, 그리고 확장된 코드가 평가된다. 모든 매크로 확장은 컴파일이 끝나기 전에 이루어지기 때문에, 런타임 시에 이루어질 연산이 컴파일 타임에 미리 일어난다면 그만큼 프로그램의 수행 성능 면에서 이득을 보게 된다. 어떤 오퍼레이터가 해야 되는 일을 부분적으로나마 매크로 확장시에 할 수 있다면 그 오퍼레이터는 매크로로 작성되는 것이 효율적이다. 런타임 시에 해야 할 일을 컴파일 타임에 할 수 있기 때문이다. avg와 같이 오퍼레이터가 해야 할 일의 일부를 매크로 확장 시에 처리하는 예들을 13장에서 살펴볼 것이다.


2. 리습과의 긴밀한 통합

때때로 함수보다 매크로를 사용하는 것이 리습과의 통합을 높여줄 때가 있다. 어떤 문제를 해결하는 프로그램을 짜는 대신에, 그 문제를 리습이 이미 해결할 줄 아는 문제로 바꾸는 매크로를 작성하는 것이다. 이런 접근 방법은, 가능한 모든 경우에, 프로그램을 더 작고 효율적으로 만든다: 리습이 당신이 해야 할 일을 대신 해주기 때문에 프로그램이 작아지고, 효율적이 된다. 이런 장점은 리습으로 임베디드 언어를 작성할 때 가장 잘 나타나는데, 이에 대해서는 19장에서 살펴본다.


3. 함수 호출을 아낄 수 있다.

매크로는 호출된 지점의 코드 속으로 확장된다. 따라서 자주 사용되는 부분의 코드를 함수가 아니라 매크로로 만든다면, 그 부분이 사용될 때마다 함수 호출이 일어나는 것을 막을 수 있다. 커먼 리습 이전의 초기 리습들에서는 이런 특징을 이용해서 런타임에 일어나는 함수 호출을 줄이곤 했었다. 하지만 커먼 리습에서는 이런 일을 함수를 inline으로 선언함으로써 할 수 있다.

함수를 inline으로 선언함으로써, 마치 매크로를 사용할 때처럼, 컴파일러로 하여금 함수가 호출된 곳으로 코드가 삽입되도록 할 수 있다. 하지만 이론과 실제는 조금 다르다; CLTL2 (p. 229)에서는 inline 선언을 컴파일러가 무시할 수 있다고 말하고 있고, 실제로 몇몇 커먼 리습 컴파일러들은 그렇게 한다. inline 선언을 무시하는 컴파일러를 사용하고 있다면, 함수 호출을 줄이기 위해 매크로를 사용할 수 있다.


몇몇 경우에는 매크로를 사용함으로써 효율성과 리습과의 긴밀한 통합이라는 두 가지 이점을 모두 얻을 수 있다. 19장의 쿼리 컴파일러의 예에서는, 모든 연산을 컴파일 타임으로 옮겨 프로그램의 수행 효율성을 높일 수 있다는 이점이 너무나 크기 때문에 프로그램 전체가 하나의 거대한 매크로로 짜여지게 된다. 이러한 효율성 외에도, 연산을 컴파일 타임으로 옮기는 것이 프로그램과 리습 사이의 통합 정도를 높이는 - 쿼리 컴파일러의 경우에 쿼리 안에서 산술 연산과 같은 리습 표현식을 쉽게 사용할 수 있게 하는 - 결과를 낳게 된다.


함수를 사용했을 때의 장점

4. 매크로는 컴파일러에게 내리는 명령에 가까운 반면에, 함수는 데이터이다.

함수는 인자로 넘겨질 수도 있고(예를 들면, apply같은 함수에), 함수에 의해 리턴될 수도 있고, 자료 구조 안에 저장될 수도 있다. 매크로는 이와 같이 할 수 없다. 하지만 어떤 경우에는, 매크로 호출을 lambda로 감싸서 함수처럼 취급하는 테크닉이 가능하다. 예를 들어, apply나 funcall을 어떤 매크로에 적용하고 싶다고 하면 다음과 같이 할 수 있다:

> (funcall #'(lambda (x y) (avg x y)) 1 3)
2

위와 같이 하는 것이 가능하긴 하지만, 꽤 불편하고, 언제나 가능한 것도 아니다. 매크로가 &rest 매개변수를 가지고 있을 경우에는 위와 같은 식으로는 가변적인 개수의 인자를 넘길 방법이 없다.


5. 소스 코드의 명확성

매크로 정의는 같은 일을 하는 함수의 정의에 비해 코드를 읽기가 어렵다. 따라서 매크로를 사용해서 얻을 수 있는 이득이 미미하다면, 함수를 사용하는 것이 낫다.


6. 함수 호출은 추적 가능하고, 따라서 디버그하기가 쉽다.

매크로는 대체로 함수보다 디버그하기가 어렵다. 매크로가 많은 코드는 실행시 에러가 발생하여 백트레이스(backtrace)를 살펴볼 때, 매크로가 모두 확장되어 있을 것이기 때문에, 자신이 애초에 짰던 코드와는 모양이 많이 다를 것이다.

매크로가 확장되면서 매크로 호출은 없어져 버린다. 따라서 프로그램의 실행시에 trace 를 사용해서 매크로 호출을 추적하는 것은 불가능하다. trace를 사용하게 되면 매크로 호출이 아니라 매크로가 확장되어 생긴 함수들에 대한 호출을 보여줄 것이다.


7. 함수는 재귀적 호출이 가능하다.

매크로에서 재귀를 사용하는 것은 함수에서처럼 그리 간단하지가 않다. 매크로를 확장하는 함수는 재귀적일 수 있을지라도, 매크로 확장 자체는 재귀적일 수 없다. 섹션 10.4에서 매크로의 재귀에 대한 주제를 다룬다.


여태까지 살펴본 장단점들을 고려하여 언제 매크로를 사용할지 결정할 수 있을 것이다. 정확한 결정을 내리기 위해서는 많은 경험이 필요한 게 사실이다. 하지만, 이후의 장들에서 매크로가 유용하게 사용되는 전형적인 예들을 많이 보게 될 것이고, 여러분이 매크로를 작성할 때 자신이 작성하려는 매크로가 그런 예들과 비슷한지를 고려함으로써 판단에 도움을 받을 수 있을 것이다.

마지막으로 위에서 나열한 장단점 중 6번은 그다지 문제가 되지 않는다는 것을 말하고 싶다. 매우 많은 매크로를 사용한 코드라 하더라도 디버그 하기가 그렇게 어렵지는 않다. 매크로 정의가 몇백줄 정도 된다면, 그런 매크로가 확장된 코드를 디버그하는 것은 매우 어려울 수 있다. 하지만 매크로를 사용해서 만드는 유틸리티들은 대부분 그 크기가 작고, 신뢰할만한 층으로서 구축된다. 긴 유틸리티라 하더라도 15줄 미만의 것이 대부분이다. 따라서 백트레이스를 살펴볼 때 이 같은 매크로들의 확장이 크게 눈을 어지럽히지는 않을 것이다.


8.3 매크로의 적용

매크로로 어떤 일들을 할 수 있는지 살펴보았다. 그렇다면 대체 어떤 종류의 어플리케이션이 매크로를 필요로 할까? 매크로의 가장 일반적인 용도는 코드의 변환이다. 코드의 변환이라는 말은 매우 단순한 일만을 가리키는 것처럼 느껴질 수 있는데, 리습의 코드는 리습의 데이터 타입 중 하나인 리스트로 되어 있기 때문에 그것은 생각보다 많은 것을 의미한다. 실제로 19장에서 24장 사이에 다루는 프로그램들의 목적은 모두 코드의 변환이기 때문에, 프로그램 전체가 매크로로 짜여지게 된다.

매크로의 종류는 while과도 같은 일반적인 목적의 작은 매크로로부터, 보다 특수한 목적을 가진 큰 매크로까지 천차만별이다. 이런 스펙트럼의 한쪽 끝에 있는 것이 리습에 내장되어 있는 매크로들과 유사한 일반적인 유틸리티들이다. 이런 유틸리티들은 대체로 작고, 일반적인 목적에 사용되며, 독립적으로 작성되어 있다. 하지만 특정 프로그램에 초점을 맞춘 유틸리티들을 작성하는 것도 가능하다. 예를 들어, 그래픽 프로그램에 적합한 유틸리티들을 매크로로 작성할 수 있다. 그런 매크로들이 많이 작성되면, 마치 리습이 그래픽 프로그램을 작성하기 위한 언어처럼 보일 것이다. 매크로가 이런 방식으로 사용되면, 마치 리습과는 전혀 다른 새로운 언어로 프로그램을 짜는 것 같이 느껴질 수도 있다. 즉, 매크로를 사용해서 임베디드 언어를 구현할 수 있는 것이다.

유틸리티는 바텀-업 스타일 프로그래밍의 결과이다. 유틸리티 층 위에서 구현되기에는 너무 작아 보이는 프로그램조차, 리습이라는 가장 낮은 레이어 위에 몇 가지 유틸리티를 추가함으로써 덕을 볼 수 있다. 인자를 nil로 셋팅하는 nil! 유틸리티는 매크로를 사용하지 않고는 만들 수 없다:

(defmacro nil! (x)
`(setf ,x nil))

nil!을 보고는 '그저 타이핑을 줄여주는 것 뿐이지 않나?'라고 생각할지도 모르겠다. 맞다. 모든 매크로가 하는 일은 그저 타이핑을 줄여주는 것이다. 하지만 컴파일러가 하는 일 역시 생각을 기계어로 옮기지 않아도 되도록 타이핑을 줄여주는 것 뿐이다. 유틸리티 각각을 보면 무시하고픈 생각이 들지 몰라도, 유틸리티들의 효과가 중첩되면 전혀 다른 결과를 만들어낸다. 간단한 매크로들이라도 몇 계층이 쌓이게 되면, 그 계층을 기반으로 똑같은 프로그램을 보다 우아하고 명쾌하게 표현할 수 있다.

대부분의 유틸리티는 패턴을 반영하고 있다. 만일 당신의 코드 속에 반복적으로 같은 패턴이 나타난다면, 그 부분을 유틸리티로 바꾸는 것이 좋다. 반복적인 패턴을 다루는 것은 컴퓨터의 일이다. 패턴을 만들어내는 프로그램을 짜는 것이 당신 스스로 매번 패턴을 작성하는 것보다 낫지 않겠는가? 프로그램을 짜는 도중에 다음과 같은 do 루프가 여러 곳에서 반복적으로 사용되고 있음을 깨달았다고 해보자:

(do ()
((not 'condition'))
'body of code')

코드에서 패턴이 반복되고 있는 걸 알았다면, 그 패턴에는 이름을 붙일 수 있는 경우가 많다. 이 패턴의 이름은 while이다. 그리고 이것을 유틸리티로 만든다면, 조건부 평가와 반복적인 평가가 필요하기 때문에 매크로로 작성해야 할 것이다. 91페이지의 내용을 바탕으로 해서 while을 정의하면 다음과 같이 정의하면:

(defmacro while (test &body body)
`(do ()
((not ,test))
,@body))

위에서 보았던 do 루프 패턴들을 while을 사용해서 치환할 수 있다:

(while 'condition'
'body of code')

결과적으로 코드는 더 짧아지고 그 의도를 더 잘 표현하게 된다.

인자들을 변환할 수 있는 매크로의 능력은 인터페이스를 작성하기에도 적합하다. 적절한 매크로는 길고 복잡한 표현식으로 되어 있는 인터페이스를 짧고 간단하게 만들 수 있다. 비록 GUI를 사용하는 엔드 유저들에게는 이런 매크로가 필요 없을지라도, 프로그래머들에게는 언제까지나 매우 유용할 것이다. 이런 예로는 defun이 있다. defun은 이름 없는 함수를 만들어 함수 이름에 바인딩하는 일을 마치 C나 파스칼에서 함수를 정의하듯이 하게 해 준다. 2장에서 살펴보았듯이 다음 두 가지 표현식은 동일한 효과를 가진다:

(defun foo (x) (* x 2))

(setf (symbol-function 'foo)
#'(lambda (x) (* x 2)))

defun은 첫번째 표현식을 두 번째 표현식으로 바꿔주는 매크로로 구현될 수 있다. 아마도 다음과 같은 매크로가 될 것이다:

(defmacro our-defun (name parms &body body)
`(progn
(setf (symbol-function ',name)
#'(lambda ,parms (block ,name ,@body)))
',name))

while이나 nil!은 일반적인 목적을 가진 유틸리티들이다. 어느 리습 프로그램이든 그것들을 사용할 수 있다. 하지만 특정한 분야를 위한 유틸리티들도 있을 수 있다. 만일 CAD 프로그램을 작성한다면, 두 개의 층으로 만드는 것이 좋을 수 있다: 먼저 CAD 프로그램을 위한 언어를 만들고(완곡한 표현을 선호한다면 툴킷이라고 할 수도 있겠다), 그 언어 위에서 CAD 프로그램을 작성하는 것이다.

리습은 다른 언어들이 당연하게 여기는 경계선들을 흐릿하게 만든다. 다른 언어들에서는 컴파일 타임과 런타임, 프로그램과 데이터, 언어와 프로그램 사이의 경계가 명확하다. 리습에서는 이런 경계가 용어의 편의를 위해서만 존재한다. 실제로 그런 경계는, 예를 들어 언어와 프로그램 사이의 경계 같은 것은, 존재하지 않는다. 문제를 해결하기 위해서 어디까지를 언어로 작성하고 어디까지를 프로그램으로 작성할 것인지는 전적으로 선택에 달려 있다. 기반이 되는 층을 언어라고 할 것이냐 툴킷이라고 부를 것이냐는 그저 용어의 문제일 뿐이다. 하지만 언어라는 용어를 사용하게 되면 리습에 유틸리티를 추가하여 특정 언어처럼 만들었듯이 해당 언어도 계속해서 확장해 나갈 수 있다는 느낌을 준다.

2D 드로잉 프로그램을 만든다고 해보자. 프로그램이 다루는 것은 시점 와 벡터 로 표현되는 선 뿐이라고 하자. 이런 프로그램은 여러 개의 개체들을 한꺼번에 움직일 수 있어야 하는데 아래의 move-objs 함수가 이런 일을 담당한다. 성능을 위해서, 개체들이 움직일 때마다 화면 전체를 갱신하지 않고, 변하는 부분만 갱신하려고 한다.

(defun move-objs (objs dx dy)
(multiple-value-bind (x0 y0 x1 y1) (bounds objs)
(dolist (o objs)
(incf (obj-x o) dx)
(incf (obj-y o) dy))
(multiple-value-bind (xa ya xb yb) (bounds objs)
(redraw (min x0 xa) (min y0 ya)
(max x1 xb) (max y1 yb)))))

(defun scale-objs (objs factor)
(multiple-value-bind (x0 y0 x1 y1) (bounds objs)
(dolist (o objs)
(setf (obj-dx o) (* (obj-dx o) factor)
(obj-dy o) (* (obj-dy o) factor)))
(multiple-value-bind (xa ya xb yb) (bounds objs)
(redraw (min x0 xa) (min y0 ya)
(max x1 xb) (max y1 yb)))))

개체들을 움직이기 전과 후에 bounds(개체의 틀이 되는 사각형의 네 꼭지점의 좌표를 리턴하는 함수)를 호출하여 비교해 보면, 변한 부분을 알아낼 수 있다. 따라서 move-objs는 개체들을 움직이기 전과 후에 bounds를 호출하고, 움직임에 따라 영향을 받은 부분만을 새로 그려야 한다.

scale-objs 함수는 개체들의 크기를 변경한다. 이 함수 역시 개체의 크기를 변경시키기 전과 후에 bounds를 호출하고 변경된 부분만을 새로 그려야 할 것이다. 우리가 계속해서 프로그램을 작성해 나감에 따라, 이런 패턴들을 무수히 많이 보게 될 것이다: 개체를 회전시키거나, 뒤집거나, 바꿔치기하는 함수들 속에서.

이 모든 함수들이 공통적으로 가지고 있는 패턴을 매크로를 사용해서 추상화시킬 수 있다. with-redraw 매크로는 위에서 본 함수들이 공통적으로 가지고 있는 패턴들을 추상화한다. 이 매크로를 사용해서, move-objs와 scale-objs를 다음과 같이 각각 네 줄로 줄일 수 있다.

(defmacro with-redraw ((var objs) &body body)
(let ((gob (gensym))
(x0 (gensym)) (y0 (gensym))
(x1 (gensym)) (y1 (gensym)))
`(let((,gob ,objs))
(multiple-value-bind (,x0 ,y0 ,x1 ,y1) (bounds ,gob)
(dolist (,var ,gob) ,@body)
(multiple-value-bind (xa ya xb yb) (bounds ,gob)
(redraw (min ,x0 xa) (min ,y0 ya)
(max ,x1 xb) (max ,y1 yb)))))))

(defun move-objs (objs dx dy)
(with-redraw (o objs)
(incf (obj-x o) dx)
(incf (obj-y o) dy)))

(defun scale-objs (objs factor)
(with-redraw (o objs)
(setf (obj-dx o) (* (obj-dx o) factor)
(obj-dy o) (* (obj-dy o) factor))))

with-redraw 매크로가 아직 두 개의 함수에 사용되었을 뿐이지만, 이미 각각의 함수에 패턴을 써 넣을 때보다 타이핑 면에서 이익이라는 것을 알 수 있다. with-redraw 매크로를 사용하는 함수가 늘어갈수록 그 이익은 더욱 커질 것이다. 그리고 가독성 면에서, 매크로를 사용하여 추상화한 코드가, 코드의 의도가 훨씬 명확하게 드러나는 것을 알 수 있다.

with-redraw를 그리기 프로그램을 작성하기 위한 언어의 일부라고 생각할 수도 있을 것이다. with-redraw와 같은 매크로가 늘어감에 따라 리습은 해당 분야에 적합한 언어처럼 변해가고, 프로그램은 애초에 그 분야만을 위한 언어로 짜여진 것 같이 우아하게 표현될 수 있다.

매크로의 주요한 용도 중 하나는 임베디드 언어를 구현하는 것이다. 리습은 프로그래밍 언어를 만들기에 매우 좋은 언어다. 리습 프로그램은 리스트로 표현되고 리습은 자체적으로 이를 다룰 수 있는 파서(read)와 컴파일러(compile)를 가지고 있기 때문이다. 대부분의 경우에는 굳이 compile을 호출할 필요도 없다; 매크로 확장이 일어난 리습 코드를 컴파일하면, 간접적으로 임베디드 언어로 짜여진 코드를 컴파일하는 것이나 마찬가지이기 때문이다(p. 225).

리습을 기반으로 구현된 임베디드 언어는 일반적인 리습의 문법과, 해당 분야에 적합한 특수한 오퍼레이터에 대한 문법이 뒤섞인 형태가 된다. 임베디드 언어를 만들기 위해서는, 리습으로 인터프리터를 작성할 수 있을 것이다. 하지만 보다 나은 방법이 있는데, 바로 변환 - 언어의 표현을 리습이 해석할 수 있는 코드로 바꾸는 - 을 사용해서 언어를 구현하는 것이다. 그리고 이런 변환을 위해서는 매크로가 필요한데, 매크로가 하는 일이 바로 어떤 표현을 다른 것으로 바꾸는 것이기 때문이다.

일반적으로 변환에 기반해서 임베디드 언어를 구현하는 것이 낫다. 그렇게 하는 것이 일단, 일이 적다. 예를 들어, 새로운 언어에 산술 연산이 필요하다면, 그것을 처음부터 모두 구현할 필요는 없을 것이다. 리습의 산술 연산 능력으로 충분하다면, 새 언어의 표현을 리습의 표현으로 바꾸는 부분만 작성하고, 실제 연산은 리습에게 맡겨두면 된다.

변환을 사용하는 것은 임베디드 언어를 속도 면에서도 빠르게 만든다. 인터프리터는 천성적으로 느릴 수 밖에 없는데, 예를 들어 코드에 루프가 있다고 하면, 컴파일 시에는 한 번만 해도 될 일들을 인터프리터는 매 루프마다 하기 때문이다. 따라서 인터프리터를 가진 임베디드 언어는, 설사 인터프리터 자체는 컴파일되어 있다 해도, 느릴 수 밖에 없다. 변환을 사용해서 언어를 구현하면, 이런 문제가 없다. 매크로를 사용해서 임베디드 언어를 구현하게 되면, 마치 작은 컴파일러를 작성한 것처럼, 성능면에서 좋은 결과를 낳는다. 사실, 임베디드 언어를 변환하는 매크로는 그 언어를 위한 컴파일러 - 단지 대부분의 일을 리습 컴파일러에게 의존하긴 하지만 - 라고 생각해도 무방하다.

19장에서 25장까지가 모두 임베디드 언어의 예를 다루고 있다. 특히 19장에서는 하나의 임베디드 언어를 인터프리터를 사용하는 방법과 변환을 사용하는 각각의 방법으로 구현하는 것을 보이고 그 차이에 대해 논의할 것이다.

어떤 커먼 리습 책에서는 CLTL1에 정의된 오퍼레이터 중 매크로의 비율이 10%도 안 된다는 것을 이유로 들어, 매크로의 사용을 예외적인 것으로 단언하고 있다. 하지만 이것은 마치 집이 벽돌로 지어져 있으니, 가구도 벽돌로 만들어져야 한다는 말과 같다. 커먼 리습 프로그램에서 매크로가 차지하는 비중은 그 프로그램의 목적이 무엇이냐에 따라 다르다. 어떤 프로그램에는 매크로가 하나도 없을 수도 있지만, 어떤 프로그램은 전체가 매크로로 짜여질 수도 있다.


translated by 찬우

2007년 12월 18일 화요일

Installing Weblocks 2

환경: Intel Macbook, Ubuntu

결국 Weblocks 하나를 위해 우분투를 깔게 되었다. 맥북에 우분투를 설치하는 것은 다음을 참조.

https://help.ubuntu.com/community/MacBook_Santa_Rosa

emacs 설치는 다음과 같이 하면 최신 버전의 emacs가 설치된다.

sudo apt-get install emacs-snapshot

SLIME 과 SBCL 설치는 맥에서 했던 것과 똑같이 하면 되고(웹에서 파일 내려받는 것은 wget 웹주소, tar.gz 파일 압축 푸는 것은 tar zxvf 파일이름), Weblocks 설치 과정도 Installing Weblocks에 적었던 것과 똑같이 하면 된다.

darcs 설치는 sudo apt-get install darcs

라이브러리들의 설치를 시도하면 libssl.so 파일이 없다면서 에러가 나는 것을 볼 수 있다. 찾아보니 cl+ssl 라이브러리가 /usr/lib 디렉토리의 libssl.so 파일을 필요로 하는데, 시스템마다 libssl 버전에 따라 이름이 다를 수 있다고 한다. 예를 들면 libssl3.so.0d와 같은 식으로 되어 있을 수 있는데 cl+ssl은 무조건 libssl.so만 찾아서 문제가 생긴다는 것이다. 따라서 파일 이름의 symlink를 만들면 문제가 해결된다고 하는데 내 경우에는 그냥 libssl3.so.0d 파일을 하나 복사해서 libssl.so라고 이름을 바꾼 다음 /usr/lib 디렉토리에 복사해 넣었더니 에러 메시지가 출력되지 않는다.

그리고 cffi library가 libtest라는 것을 요구하면서 디버거로 떨어지는 일이 생기는데, 구글 그룹에 질문했지만 원인을 찾지 못했다. 일단 무조건 continue를 눌러 진행해도 weblocks 실행에 문제는 없어보인다.

후.. 드디어 Weblocks가 돌아간다.

Installing Weblocks

환경: Intel Macbook, Leopard

Weblocks는 continuation, widget 기반의 Common Lisp 웹 프레임워크다. 매뉴얼을 읽어보면 아이디어나 구조가 꽤나 깔끔해 보이기 때문에 흥미가 간다.

http://trac.common-lisp.net/cl-weblocks/wiki/ObtainingAndInstalling

위 사이트를 참조해서 설치했다. 먼저 소스코드를 내려받기 위해서 darcs를 깔아야 한다. 맥용 설치 파일은 여기에 있다. 내려받아서 압축 풀고 더블 클릭해서 설치하면 된다.

그런 다음 터미널에서

darcs get http://common-lisp.net/project/cl-weblocks/darcs/cl-weblocks

를 입력하면 소스 코드 트리를 내려받는다.

이제 Weblocks가 필요로 하는 다른 파일들을 다운받아야 하는데 asdf (rubygem같이 리습 라이브러리들을 온라인으로 쉽게 내려받아 설치하게 해주는 도구. SBCL에는 기본으로 깔려 있다)를 이용해서 받으면 된다. SLIME(또는 REPL)에서 다음과 같이 입력한다.

(require 'asdf-install)
(loop for i in '(:closer-mop :metatilities :hunchentoot :cl-who :cl-ppcre :cl-json :puri :rt :tinaa :md5 :cl-fad :fare-matcher :cl-cont :cl-prevalence) do (asdf-install:install i))

그러면 하나 하나 설치할 때마다 시스템에 설치할 것인지 로컬에 설치할 것인지 물어보는데, 내 경우에는 2번을 선택해서 로컬에 설치하였다. GnuPG가 깔려 있어서 디버거로 떨어지면 무조건 0번을 선택해서 검사를 패스한다. 그리고 설치하다가 권한 문제로 설치가 안 되는 경우는 su로 권한을 얻으려고 하면 맥에서는 기본적으로 su가 막혀있기 때문에('su 사용자이름'은 먹는다. 이렇게 해도 될 것 같긴 한데..) 아예 emacs를 실행시킬 때 sudo emacs로 실행을 해서 SLIME을 띄우면 설치에 문제가 없는 듯 하다.

그리고 Lisp implementation startup file에 다음 코드를 추가하라고 한다.

(push #p"/path/to/cl-weblocks/" asdf:*central-registry*)

나는 SBCL을 사용하기 때문에 .sbclrc 파일을 홈 폴더에 만들어서 다음과 같이 추가하였다 (물론 "/path/to/"자리에는 자신의 cl-weblocks 폴더가 있는 경로를 적어야 한다). SLIME이 실행될 때 .sbclrc 파일은 자동으로 로드된다(사실 그냥 리습 파일을 하나 만들어서 필요할 때마다 로드해도 된다; .sbclrc 파일을 만들어서 좋은 점은 자동으로 로드된다는 것 뿐인 것 같다;).

(require 'asdf)
(push #p"/Users/chanwoo/work/cl-weblocks/" asdf:*central-registry*)

그리고 SLIME에서 다음과 같이 입력하면,

(asdf:operate 'asdf:load-op 'weblocks)

드디어 Weblocks를 사용할 수 있다!

그럼 사이트에 나온 Hello 예제를 해보자. SLIME에서 (weblocks:start-weblocks) 를 입력하고 브라우저 주소 창에 http://localhost:8080/ 을 치면, 초기 화면이 뜨는 것을 볼 수 있다.

다음 코드를 입력한 후 브라우저를 새로 고침 하면 Hello가 보인다.

(weblocks:defwebapp 'our-application)
(defun init-user-session (comp)
(setf (weblocks:composite-widgets comp)
(list "Hello!")))
(weblocks:reset-sessions)

하지만 weblocks를 start 시킨 이후에 CL-USER> 프롬프트가 금방 나타나지 않고, 엔터를 몇 번 치면 나타나지만 입력을 제대로 먹질 않고 다음과 같은 메시지만 뜬다.

pipelined request... (swank:listener-eval ~~~

도대체 왜 이런지를 알 수가 없어서 구글 Weblocks 그룹에 질문했는데, SBCL이 맥에서는 스레드를 지원하지 않기 때문이란다; 맥에서 스레드를 지원하는 구현을 찾아 새로 까는 수 밖에 없다; 그래서 OpenMCL(Clozure CL - CCL)을 깔아봤지만 CCL은 asdf-install과 관련된 문제가 너무 많이 발생한다. SBCL에서는 멀쩡히 깔리던 라이브러리들이 CCL에서는 온갖 에러를 발생시킨다; 후.. 결국 개발에 제일 편한 것은 리눅스였던 것이다. 결국 맥에 부트캠프로 우분투를 깔게 되었다.

2007년 12월 14일 금요일

Installing Common Lisp - OpenMCL (Clozure CL)

후.. 맥에서는 SBCL이 멀티 스레드를 지원하지 않는다고 해서 OpenMCL을 깔게 되었다.

환경: Intel Macbook, Leopard

일단 OpenMCL 홈페이지는 여기: http://trac.clozure.com/openmcl

다음 ftp 주소 - ftp://clozure.com/pub/testing/ - 로 가면 파일들이 주욱~ 보인다. 설치할 수 있는 것은 다음 두 가지다. SLIME에서 프로그래밍하려면 2번만 깔면 되지 않나 싶다.

1. ClozureCL2007-12-03.dmg
이건 SLIME에서 불러올리는 리습 구현이 아니라 응용 프로그램에서 단독으로 REPL을 실행하게 해주는 파일이다. 다운받아서 더블 클릭한 다음, 마운트 되면 생기는 Clozure CL 아이콘을 응용 프로그램 폴더에 끌어다 놓으면 설치가 된다.

2. openmcl-darwinx8664-snapshot-070722.tar.gz
다운받아 압축을 풀면 ccl 폴더가 생기는데 아무데나 위치해도 상관 없다. 단, 다음과 같은 설정을 해주면 된다.

다운로드 받은 소스 파일은 2007년 7월의 스냅샷으로, 그 이후에도 많은 변경이 있기 때문에 업데이트와 리빌드가 필요하다. 방법은 다음 링크를 참조: http://trac.clozure.com/openmcl/wiki/UpdatingFromSource
(2008년 1월 16일 시점으로 리빌드 후 정상적으로 슬라임이 시동되지 않는 것 같다. 그래서 업데이트 하지 않은 스냅샷을 그대로 사용중이다.)

ccl/scripts/ 폴더 안에 openmcl64 파일이 있는데 이 파일을 열면CCL_DEFAULT_DIRECTORY=/usr/local/src/ccl 과 같이 되어 있는 것을 볼 수 있다. 이 부분을 CCL_DEFAULT_DIRECTORY= "ccl 폴더가 있는 위치" 로 변경하고 저장한다.내 경우에는 귀찮아서 그냥 ccl 폴더를 /usr/local/src/ccl 에 위치시켰다.
(인스톨 방법은 다음 링크를 참조하였다: http://openmcl.clozure.com/Doc/index.html)

그리고 홈폴더(~)의 .emacs 파일(없으면 새로 만들면 된다)에 다음과 같이 추가한다.

(add-to-list 'load-path "SLIME이 있는 경로")
(setq inferior-lisp-program "ccl폴더가 위치한 경로/scripts/openmcl64")
(require 'slime)
(slime-setup)

내 경우에는 다음과 같다.

(add-to-list 'load-path "/usr/local/bin/slime-2.1")
(setq inferior-lisp-program "/usr/local/src/ccl/scripts/openmcl64")
(require 'slime)
(slime-setup)

그리고 이맥스를 실행시키고 meta+x slime 을 치면 openmcl REPL이 등장하는 것을 볼 수 있다(meta 키는 터미널에서는 alt/option 키이다. 먹히지 않으면 메뉴의 "터미널 -> 환경설정 -> 키보드 -> 'option을 메타키로 사용'에 체크").

그리고 openMCL은 asdf(리습 라이브러리를 손쉽게 설치하게 도와주는 도구)를 포함하고 있지만, asdf-install을 사용하기 위해서 약간의 설정이 필요하다. 홈폴더에 openmcl-init.lisp 파일을 만들어서 다음 코드를 추가한다(openmcl-init.lisp 파일은 SLIME이 실행될 때 자동으로 로드되게 된다).

(require 'asdf)
(pushnew "ccl:tools;asdf-install;"
asdf:*central-registry* :test #'string-equal)
(asdf:operate 'asdf:load-op 'asdf-install)

2007년 12월 12일 수요일

Common Lisp Web Development

Common Lisp으로 웹 프로그래밍을 해보려고 웹 개발 프레임워크를 찾아보았다.

눈에 띄는 것은 다음 세 가지 정도다.

1. UnCommon Web
제일 이름이 눈에 많이 띄는 웹 프레임워크. TBNL이라는 것을 전신으로 만들어진 듯. continuation(간단히 말하면, computation의 특정 순간을 저장하는 일급 객체 - 자료구조에 저장되거나 함수의 인자나 리턴값으로 사용될 수 있는 - 라고 할 수 있을 듯. Scheme에서 continuation을 일급 객체로 지원하고 Common Lisp에서는 클로저와 매크로를 사용해서 continuation을 흉내낼 수 있다. Ruby에서도 지원하는 듯. 아무 상태나 저장하는 게 가능하기 때문에 session이나 cookie보다 강력한 상태 저장 수단으로서 웹 개발에서 쓰이는 듯하다. 확실친 않지만 웹 어플리케이션을 그저 함수의 연속으로 바라볼 수 있게 만드는 강력한 추상화 수단일수도..)을 기반으로 웹 어플리케이션의 control flow를 제어하는 듯. 문서도 어느 정도 있는 듯 하지만, out of date된 것이 좀 있는 것 같고, 결정적으로 간단 설치 버전이 뭔가 잘 작동하지 않는다. 이게 안 되면 일일이 다 설치해 주어야 하는데, 후.. 추후에 다시 시도해봐야 할 듯하다.

2. WebBlocks
최근에 만들어지고 있는 프레임워크인 듯. 역시 continuation을 기반으로 control flow를 제어한다. 아무래도 새로 만들어지고 있는 프레임워크인만큼 문서가 많지는 않은 듯 하다. 하지만 구글에 토론 그룹이 있고, 문서가 부족하면 코드를 보자라는 생각으로 코드를 들여다보고 있다. 꽤나 흥미로워 보이는 프로젝트다.

2008.3.19 시점: Weblocks를 주로 보고 있다. 아이디어가 매우 훌륭한 것 같고, 여러 가지를 배울 수 있는 프레임워크라고 생각한다. 커뮤니티도 활발히 살아있는 편이다.

3. Hunchentoot
프레임워크라기 보다는 가벼운 웹 서버 겸 개발 도구인 것 같다. 일단 설치에 성공했기 때문에 적어보려 한다. 리습 구현으로 SBCL을 사용하면 asdf(인터넷으로 Common Lisp 라이브러리들을 받아서 설치해 주는 도구)가 기본적으로 깔려 있기 때문에 slime에서 그저 다음과 같이 치면 된다. (설치 매뉴얼을 살펴보면 GnuPG를 먼저 깔라고 하는데, asdf를 사용해서 라이브러리를 인스톨할 때 악성 코드인지를 검사하는 도구인 것 같다. 근데 깔면 설치시에 일일이 디버거로 떨어져서 검사할 거냐고 물어보기 때문에 귀찮은 것 같다. 그냥 안 까는 게 나은 듯; 만일 깔아서 설치 도중 자꾸 디버거로 떨어진다면 0을 눌러서 GnuPG 검사를 건너뛰면 된다. 계속 눌러줘야 된다는 게 짜증나지만;)

(require 'asdf-install)
(asdf-install:install 'hunchentoot)

그럼 시스템에 깔 것인지 개인 사용자 폴더에 깔 것인지 물어보는데 시스템에 깐다고 하면 뭔가 꼬여서 그냥 2번을 선택했다.

설치가 완료되었다. LispCast(Hunchentoot를 사용해서 Reddit이라는 웹 사이트의 간단 버전을 만드는 동영상들이 올라와 있다)에서 받은 첫 번째 동영상대로 따라 해 보니 Server가 시작된다.

하지만 Server가 시작되고나서 슬라임의 프롬프트가 바로 돌아오지 않고 명령을 먹어버리는 현상이 발생하는데 이는 SBCL이 맥에서 스레드를 지원하지 않기 때문이란다. 정확하게 말하자면 스레드를 지원하지 않는 것이 아니라 experimental 스레드를 지원한다고 한다. 그래서 스레드를 enable시키는 방법을 찾아봤는데, 그 방법이란 게 SBCL 인스톨 문서의 2.2 부분에 간단히 적혀 있는 것이다. 그 방법대로 해 보았지만 역시 제대로 동작하는 것 같지가 않다. 그리고 ReadyLisp이라고 해서 Emacs와 SLIME, SBCL을 하나로 묶고 인텔 맥에서 멀티 스레드를 지원하도록 해서 쉽게 설치할 수 있게 배포하는 사이트(http://www.newartisans.com/software/readylisp.html)가 있는데, 그냥 창을 닫거나 메뉴에서 종료를 선택하면 제대로 종료되지가 않고 ctrl+x ctrl+c 로 종료해야 정상적으로 종료되는 것 같다. 멀티 스레드가 되는지는 아직 시험해 보지 못했다.

Packages (패키지)

1. 개념

패키지란 심볼들을 모아놓은 것이다. 리습에서 심볼이란 그저 변수 또는 함수의 '이름'이라고 생각하면 무방하다. 여러 명이 같이 코딩을 할 때 변수의 이름으로 같은 심볼을 사용한다면 이름 충돌이 생길 수 있다. 패키지는 이름 공간(name space)의 분리를 통해 이와 같은 충돌을 막는다. 다음 코드를 보자.

? (make-package :bob)
#
? (make-package :jane)
#
? (in-package bob)
#
? (defun foo () "This is Bob's foo")
FOO
? (in-package jane)
#
? (defun foo () "This is Jane's foo")
FOO
? (foo)
"This is Jane's foo"
? (in-package bob)
#
? (foo)
"This is Bob's foo"

밥과 제인은 bob과 jane이라는 각자의 이름 공간을 만들고, 각자의 이름 공간 안에서 foo라는 함수를 정의하였다. (in-package bob) 뒤에 오는 코드들은 bob 패키지의 이름 공간 안에서 읽히게 된다. 밥이 제인의 foo를 호출하려면 다음과 같이 하면 된다.

? (in-package bob)
#
? (jane::foo)
"This is Jane's foo"

또는 import를 사용해서 :: 없이 심볼을 참조할 수도 있다.

? (in-package jane)
#
? (defun baz () "This is Jane's baz")
BAZ
? (in-package bob)
#
? (import 'jane::baz)
T
? (baz)
"This is Jane's baz"

심볼을 특정한 패키지로 집어넣는 것을 intern 시킨다고 한다. 하지만 intern이라는 함수는 없고, intern시키기 위해서는 위에서와 같이 import를 사용해야 한다. 어느 패키지에도 속하지 않는 심볼을 uninterned 되었다고 하는데, uninterned된 심볼은 평가되면 앞에 #: 이 붙는다(사실 이는 정확한 설명이 아닌데, 정확하게는 홈 패키지가 없는 심볼에 #:이 붙는다). 다음 예를 보면, 어떤 패키지에도 속하지 않은 symbol1을 평가하면 앞에 #:이 붙지만 symbol1을 import 하면(common-lisp-user 패키지로 intern 시킨 것이다) #:이 사라지는 것을 볼 수 있다.

? symbol1
#:MY-SYMBOL
? (import symbol1)
T
? symbol1

import의 반대는 뭘까? 이름에 일관성이 없지만, unintern이다. 어떤 심볼을 패키지에서 제거하기 위해 unintern을 사용한다.

그리고, Home Package란 개념이 있다. A라는 심볼이 있는데 그 심볼이 처음으로 만들어진 패키지가 B라고 하자. 그리고 C라는 패키지가 A라는 심볼을 B에서 import 했다고 하자. 이럴 경우 A가 B에서 처음 만들어졌기 때문에 B가 A의 home 패키지가 된다. 이 경우에 D라는 패키지에서 A를 참조하려고 한다면 다음과 같이 할 수 있다.

B:A 또는 C::A

즉, 홈 패키지의 심볼을 참조할 때는 콜론(:)을 하나만 붙여도 된다.

2. 실제로 알아야 하는 것

실제로 프로그램을 짤 때는 intern 등을 직접 사용하기보다 defpackage를 사용한다. defpackage는 패키지를 정의하면서 그 패키지가 어떤 패키지를 사용(다른 패키지를 사용한다는 것은 그 패키지의 심볼들을 참조할 수 있다는 것이다)하며 그 패키지의 어떤 심볼들을 외부에서 사용할 수 있게 허용할 것인지를 정의한다. 다음을 보자.

(defpackage :com.gigamonkeys.text-db
(:use :common-lisp)
(:export :open-db
:save
:store))

이 코드는 com.gigamonkeys.text-db 라는 패키지를 정의하고 있는데, 이 패키지는 common-lisp이라는 패키지를 사용하며(따라서 그 패키지 안에 있는 모든 심볼들을 : 없이 접근 가능하다. common-lisp 패키지는 커먼 리습 언어의 모든 기본적인 심볼들을 가지고 있는 패키지이다) open-db, save, store라는 패키지 내의 심볼들을 외부(즉, com.gigamonkeys.text-db라는 패키지를 사용한다고 선언한 패키지들)에서 사용할 수 있게 제공한다.

그리고 다음 코드를 보자.

(defpackage :com.gigamonkeys.email-db
(:use
:common-lisp
:com.gigamonkeys.text-db
:com.acme.text)
(:import-from :com.acme.email :parse-email-address)
(:shadow :build-index)
(:shadowing-import-from :com.gigamonkeys.text-db :save))

:import-from은 com.acme.email 패키지 전체를 사용하는 것이 아니라 그 패키지의 parse-email-address 심볼만 가져오겠다는 것이다. 반대로 :shadow는 build-index라는 심볼만 사용에서 제외시키고 싶을 때 사용한다. 그리고 (:shadowing-import-from :com.gigamonkeys.text-db :save) 는 save라는 심볼이 com.gigamonkeys.text-db에도 있고 com.acme.text에도 있을 때 두 패키지 중 com.acme.text의 save가 아니라com.gigamonkeys.text-db의 save 심볼을 가져오겠다는 것이다.

do-symbols, do-external-symbols 등은 특정 패키지 안의 심볼들 전부나, 특정 패키지 안에서 export한다고 선언한 심볼들 전체를 이터레이트하면서 무언가를 하도록 선언하는 매크로이다. 자세한 것은 Common Lisp HyperSpec을 참조하길 바란다.

References
Ron Garret의 홈페이지 - The Idiot's Guide to Common Lisp Packages
http://www.flownet.com/ron/
Practical Common Lisp 21장 - Programming in the Large: Packages and Symbols
http://www.gigamonkeys.com/book/programming-in-the-large-packages-and-symbols.html
Common Lisp HyperSpec
http://www.lispworks.com/documentation/HyperSpec/Body/m_do_sym.htm

Continuation

continuation이란 미래에 될 computation 과정을 얼려놓은 것을 말한다. 프로그램의 실행 도중에 이 지점 이후에 실행될 과정을 모조리 얼려놓은 객체랄까? 다음 예를 보자.

(+ 1 (call/cc
(lambda (k)
(+ 2 (k 3)))))

=> 4

call-with-current-continuation, 줄여서 call/cc라고 하는 오퍼레이터는 '인자가 하나인 함수'를 인자로 받는다. call/cc를 호출하게 되면 call/cc가 호출된 지점을 제외한 '나머지 부분'이 call/cc의 인자로 넘겨진다. 즉 lambda의 인자 k에는 프로그램에서 call/cc를 제외한 나머지 부분인 (+ 1 ~~) 라고 하는 계산 과정이 저장되게 된다(이 저장된 객체를 continuation이라고 한다). (k 3) 이 평가되면 3이 저 ~~ 자리에 들어가서 이 코드의 실행 결과는 4가 된다. (+ 2 부분이 무시되는 이유는 continuation이 저장된 시점의 스택 상태에서 계산이 되기 때문이다.

다른 예를 보자.

(define r #f)

(+ 1 (call/cc
(lambda (k)
(set! r k)
(+ 2 (k 3)))))

=> 4

(r 5)

=> 6

(+ 3 (r 5))

=> 6

여기서는 (+ 1 ~~) 라는 continuation 객체를 r 이라는 변수에 저장하였다. 따라서 (r 5) 가 평가될 때 r은 (+ 1 ~~) 이고 머시기 자리에 5가 들어가기 때문에 평가한 결과는 6이 된다.

continuation 은 scheme에서 처음으로 일급 객체(자료구조에 저장될 수 있고, 함수의 인자나 리턴 값이 될 수 있는 객체)로 제공하기 시작한 것 같다. 예에서 본 바와 같이, 제어 흐름을 특정 시점의 스택으로 돌려놓기 때문에 루프에서 탈출하는 오퍼레이터를 만들거나 할 때 쓰인다고 한다.

하지만 이 개념이 각광을 받는 부문은 웹 개발 쪽인 것 같다. 연산 과정 중 임의의 포인트를 저장할 수 있다는 특성을 이용해서 HTTP의 상태 없음을 극복해 보겠다는 것인데, session이나 cookie 같은 경우는 디자이너가 어떤 식으로 상태를 저장하고 관리해야 할지 설계해야하는 반면에 continuation은 완전히 arbitrary한 포인트들을 저장할 수 있고, 상태 저장을 단순한 문제로 바꾼다는 것 같다. 2001년도부터 2007년까지 ICFP(International Conference on Functional Programming - ACM 쪽이다)쪽에 continuation을 이용한 웹 개발의 장점들에 대해 논하는 논문들이 실려왔다.

continuation을 이용하는 시스템들은 다음과 같다.

Yahoo! Store, PLT Scheme Web Server, Uncommon Web framework and Weblocks Web framework(for Common Lisp), Seaside Web Server(for Smalltalk), Apache Cocoon Web application framework

주로, lisp을 사용하는 소프트웨어들이다. 커먼 리습에서 continuation을 일급 객체로 지원하진 않는 것 같지만 Paul Graham의 On Lisp 20장을 읽어보면 클로저와 매크로를 이용해서 continuation을 구현하는 것을 코드로 보이고 있다. 루비도 continuation을 지원하긴 하던데, 레일스는 continuation 기반 상태 저장을 지원하는지 잘 모르겠다.

References

Teach Yourself Scheme in Fixnum Days의 call-with-current-continuation
Wikipedia - continuation
Paul Graham의 On Lisp - chapter 20에 자세한 설명이 나와 있다