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
포함되어 있는 여러 가지 문서의 인덱스를 보여준다.

Mysql 설치 및 실행

환경: 인텔 맥, 레오파드

다운 받는 곳
http://dev.mysql.com/downloads/mysql/5.0.html#macosx-dmg

package format을 받은 후 더블 클릭하여 설치하면 /usr/local/mysql-VERSION 에 설치가 되고 symbolic link로 /usr/local/mysql 에도 폴더가 생긴다. 처음에는 똑같은 파일이 두 곳에 있는 것을 보고 이미 기존에 설치되어 있었는데 또 설치한 건가 하는 생각을 했다.

mysql을 start up 시키려면 다음과 같이 한다.
shell> cd /usr/local/mysql
shell> sudo ./bin/mysqld_safe

잠시 실행을 중단하려면
control + z

실행을 재개하려면
shell> bg

shell에서 로그아웃하려면
control + d 또는 exit를 친다

맥에서는 새 버전의 mysql을 설치하면 기존 설치된 mysql이 업그레이드 되는 것이 아니라 기존 버전은 그대로 둔 채 새로 설치를 해버린다고 한다. 따라서 새로운 버전을 설치했을 경우 기존 버전의 파일들을 지워야 한다. 또한 다음 파일도 지워야 한다고 한다.
/Library/Receipts/mysql-VERSION.pkg

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일 화요일

Blogger, Indentation, Code Spacing

Blogger에서 새 게시물을 작성하게 되면 HTML 편집 모드와 쓰기 모드가 있는 것을 볼 수 있다. 쓰기 모드에서 프로그램 코드를 붙여넣고 글을 게시하게 되면 모든 들여쓰기를 무시해버리기 때문에 도무지 코드를 읽을 수가 없다. 방법은 아예 처음부터 HTML 편집 모드에서 글을 작성하는 것이다. line breaking 옵션에 체크해두면 HTML 모드에서도 라인 간 공백은 인식하기 때문에 일일이 br 태그를 쓸 필요 없이 그냥 글을 작성하면 되고 코드를 붙여넣을 때만 코드 앞 뒤를 pre 태그로 감싸주면 코드의 들여쓰기가 무시되지 않는다.

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에서는 온갖 에러를 발생시킨다; 후.. 결국 개발에 제일 편한 것은 리눅스였던 것이다. 결국 맥에 부트캠프로 우분투를 깔게 되었다.

우분투(Ubuntu)에서 구글 툴바의 즐겨찾기 문제

우분투에서 구글 툴바를 깔면 "즐겨찾기를 다운로드하고 있습니다"라는 메시지만 보이면서 즐겨찾기가 제대로 표시되지 않는 것을 볼 수 있다. 이 경우에는 상단 메뉴의 '시스템-관리-시냅틱 꾸러미 관리자'를 선택한 후 libstdc++로 검색해서 libstdc++5 설치에 체크한다. 그러면 의존성 관계에 의해 gcc-3.3-base 도 설치할 거냐고 물어보는데 역시 선택한다. 이 두 항목이 체크되면 메뉴의 '적용'을 누른다. 이 둘을 설치한 후 구글 툴바를 다시 설치하면 즐겨찾기가 제대로 보이는 것을 볼 수 있다.

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

퀵타임(QuickTime)으로 ogg 파일 재생하기

LispCast에서 동영상 파일을 받았는데 ogg 파일이다. 퀵타임으로, 무비스트로도, 패럴렐즈에서 곰플레이어로도, 재생이 안 된다. 다행히 퀵타임에서 ogg 파일을 재생할 수 있게 해주는 플러그인을 찾았다. XiphQT dmg 파일을 받아서 더블 클릭한 후 나타나는 XiphQT 파일을 홈폴더의 라이브러리/Components 디렉토리(없으면 만들어주면 된다)에 복사하면 설치 끝이다. 이제 퀵타임으로 ogg 파일 재생이 된다.

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에 자세한 설명이 나와 있다

Closure (클로저)

클로저란 함수나 코드 블록이 생성될 당시의 바인딩 정보를 같이 갖고 있는 것이라고 생각하면 된다. 다음 예를 보자.

(let ((counter 0))
(defun reset ()
(setf counter 0))
(defun stamp ()
(setf counter (+ counter 1))))

> (list (stamp) (stamp) (reset) (stamp))

(1 2 0 1)

지역변수 counter의 값이 0으로 설정된 상태에서 counter를 0으로 셋팅하는 함수 reset과 counter 값을 1 늘리는 stamp 함수를 선언하였다. stamp함수를 호출할 때마다 counter 값이 늘어나는 것을 볼 수 있다. 지역변수가 소멸하지 않고 바인딩이 계속 살아 있는 것이다. 하지만 counter는 지역 변수이기 때문에 외부에서 접근하거나 볼 수가 없다. 오로지 reset과 stamp를 통해서만 접근할 수 있는 것이다.

마치 가방 안에 어떤 물건을 넣고 가방을 닫으면(close over => closure) 가방을 열 때도 그 물건이 있는 것과 같이 함수나 코드 블록이 생성될 때 특정 지역변수에 대한 바인딩이 있으면 함수와 바인딩이 같이 살아있게 되는 것을 가리켜 클로저라고 하는 것 같다.

다른 예를 보자.

(defun make-adder (n)
#’(lambda (x)
(+ x n)))

> (setf add3 (make-adder 3))

#

> (funcall add3 2)

5

> (setf add27 (make-adder 27))

#

> (funcall add27 2)

29

make -adder는 n을 인자로 받아서 '어떤 인자를 받으면 n을 더해서 리턴하는 함수'를 리턴하는 함수다. (setf add3 (make-adder 3))은 3을 더해서 리턴하는 함수를 만들어서 add3이라는 변수에 저장한 것이다. add3과 add27을 보면 각각의 함수가 그 함수가 만들어질 때 파라미터로 주어진 n 값의 바인딩을 그대로 가지고 있는 것을 볼 수 있다.

10. 매크로 - ANSI Common Lisp 번역

10. 매크로

리습 코드는 리습 객체 중 하나인 리스트로 표현된다. 섹션 2.3에서 리습 코드가 리스트로 표현될 수 있기 때문에 프로그램을 만드는 프로그램을 작성하는 일이 가능하다고 언급했었다. 이 장에서는 어떻게 그런 일이 가능한지를 살펴보려고 한다.


10.1 Eval

표현식을 어떻게 만드는지는 명확하다: 그저 list 를 호출하면 된다. 하지만 아직까지 우리는 어떻게 해야 리습이 리스트를 코드로 받아들이게 할 수 있는지를 살펴보지 않았다. 함수 eval 이 바로 그와 같은 일을 하는데, eval은 표현식을 받아서, 그것을 평가하고, 평가한 값을 리턴한다:

> (eval '(+ 1 2 3))
6
> (eval '(format t "Hello"))
Hello
NIL

별로 새로워 보이지 않는다. 우리가 여태까지 탑레벨에서 이야기해 온 상대가 바로 eval 이었다. 다음 함수는 탑레벨을 거의 유사하게 구현한다:

(defun our-toplevel ()
(do ()
(nil)
(format t "~%> ")
(print (eval (read)))))

이 함수를 보면 탑레벨이 왜 read-eval-print loop로 불리는지 알 수 있을 것이다.

eval을 호출하는 것은 리스트를 코드로 바꾸는 방법 중 하나이다. 하지만 그리 썩 좋은 방법은 아니다:

1. 일단 느리다: eval은 리스트를 받아서 그 시점에 컴파일하거나 인터프리터에서 평가한다. 어떤 식으로 하든 이미 컴파일된 코드가 실행되는 것보다는 매우 느릴 수 밖에 없다.

2. eval로 넘겨지는 표현식이 문맥 속에서 평가될 수 없다는 것도 문제다. eval을 let 안에서 호출한다고 해도, eval로 넘겨지는 표현식에서 let이 설정한 변수를 참조할 수 없다.

코드를 만들어내는 보다 나은 방법을 다음 섹션에서 설명할 것이다. eval을 사용하기에 적합한 곳은 탑레벨 루프 정도 뿐이다.

프로그래머에게 eval은 리습을 모사하는 모델로서 쓸모가 있다. 우리는 eval이 다음과 같은 cond 표현식으로 되어 있을 거라고 상상할 수 있다.

(defun eval (expr env)
(cond ...
((eql (car expr) 'quote) (cadr expr))
...
(t (apply (symbol-function (car expr))
(mapcar #'(lambda (x)
(eval x env))
(cdr expr))))))

대부분의 표현식은 default 절(t ... )에 의해 처리될 것이다. 표현식에서 car로 함수를 뽑아내고 cdr로 뽑아낸 각 인자들을 평가해서, 함수를 각 인자들에 적용한 결과를 리턴하는 식이다.

하지만 표현식이 (quote x)와 같은 경우에는 quote가 인자를 평가되지 않도록 보호하기 때문에 이런 식으로 처리되지 않는다. 따라서 우리는 quote를 위한 별개의 절이 필요하다. 특수 오퍼레이터들은 quote 처럼 eval 안에서 별개의 절을 가지게 된다.

coerce 와 compile 역시 리스트를 코드로 바꿀 수 있다. coerce를 이용해 람다 표현식을 함수로 바꿀 수 있다.

> (coerce '(lambda (x) x) 'function)
#

그리고 compile의 첫 번째 인자로 nil을 주게 되면 compile은 두 번째 인자로 주어진 람다 표현식을 컴파일한다.

> (compile nil '(lambda (x) (+ x 2)))
#
NIL
NIL

coerce와 compile이 리스트를 인자로 받기 때문에 프로그램이 실행 중에 새로운 함수를 만드는 것이 가능하다. 하지만 이런 것은 eval을 사용하는 것과 같이 매우 드문 경우다.

eval, coerce, compile의 문제점은 리스트를 코드로 바꿀 수 있다는 데 있는 게 아니라, 런타임에 그와 같은 일을 한다는 데 있다. 런타임에 그와 같은 일을 하는 것은 프로그램의 수행 속도를 크게 떨어뜨린다. 그와 같은 일을 컴파일 타임에 한다면, 프로그램이 실제로 실행될 때는 속도 저하가 전혀 없을 것이다. 다음 섹션에서 우리가 하고자 하는 일이 바로 이런 것이다.


10.2 매크로

프로그램을 짜는 프로그램을 만드는 것은 대부분 매크로를 사용해서 이루어진다. 매크로는 변환을 규정하는 오퍼레이터이다. 따라서 매크로를 정의한다는 것은 매크로를 호출할 때 어떤 식으로 변환이 일어날지를 규정하는 것이다. 이 같은 변환을 매크로 확장(macro-expansion)이라고 하는데, 이는 컴파일러에 의해 행해진다. 따라서 매크로에 의해 생성된 코드는 직접 타이핑한 코드와 마찬가지로 프로그램의 일부가 된다.

일반적으로 defmacro 를 사용해서 매크로를 정의할 수 있다. defmacro는 defun과 비슷해 보이지만, defun은 호출의 결과가 어떤 값을 낳아야 하는지를 정의하는 반면에, defmacro는 호출의 결과가 어떤 변환을 유발하는지를 정의한다. 예를 들어, 받은 인자를 nil로 만드는 매크로는 다음과 같다:

(defmacro nil! (x)
(list 'setf x nil))

이 코드는 하나의 인자를 받는 nil!이라는 오퍼레이터를 정의하고 있다. (nil! a)과 같은 코드는 컴파일되거나 평가되기 전에 (setf a nil)과 같은 코드로 변환될 것이다. 탑레벨에서 (nil! x)를 쳐보자,

> (nil! x)
NIL
> x
NIL

(setf x nil)을 친 것과 정확히 같은 결과가 나온다.

함수를 테스트하기 위해서는, 함수를 호출해봐야 한다. 매크로를 테스트해보기 위해서는 매크로가 어떻게 확장되는지를 봐야 한다. 함수 macroexpand-1은 매크로 호출을 받아서 매크로 확장의 결과를 보여준다:

> (macroexpand-1 '(nil! x))
(SETF X NIL)
T

매크로 호출이 또 다른 매크로를 호출할 수 있을 것이다. 컴파일러나 탑레벨이 매크로 호출을 만나면 더 이상 매크로 호출이 일어나지 않을 때까지 계속해서 매크로를 확장한다.

매크로를 이해하기 위해서는 매크로가 어떻게 구현되어 있는지를 살펴보는 것이 도움이 된다. 내부적으로, 매크로는 그저 표현식을 변환하는 함수일 뿐이다. 예를 들어, (nil! a)라는 표현식을 다음 함수에 넘기면

(lambda (expr)
(apply #'(lambda (x) (list 'setf x nil))
(cdr expr)))

함수는 (setf a nil)을 리턴할 것이다. defmacro를 사용하게 되면, 이와 같은 함수를 정의하게 되는 것이다. macroexpand-1이 하는 일은 어떤 표현식을 받아서 표현식의 car가 매크로의 이름이라면 위와 같은 함수에 표현식을 넘기는 것이 전부다.


10.3 Backquote

backquote read-macro 는 템플릿으로부터 리스트를 생성해낸다. backquote는 특히 매크로 정의에서 많이 쓰인다. quote는 닫힌 따옴표(apostrophe 역주: ' 엔터키 바로 왼쪽 키)인 반면에 backquote는 열린 따옴표로 표시된다.(역주: ` 숫자 1 키의 바로 왼쪽 키) backquote라고 불리는 이유는 일반 quote의 방향과 반대 방향을 바라보는 것처럼 생겼기 때문이다.

backquote가 단독으로 쓰이면, quote와 똑같은 역할을 한다:

> `(a b c)
(A B C)

quote와 마찬가지로, backquote 역시 인자를 평가로부터 보호한다.

backquote가 가진 장점은 backquote 표현식 내에서 , (comma) 와 ,@ (comma-at)을 사용해서 일부만 평가가 되도록 할 수 있다는 것이다. backquote 표현식 내에서 어떤 것 앞에 , 를 붙이게 되면 그것은 평가된다. backquote와 콤마를 동시에 사용해서 리스트를 만들어 보자:

> (setf a 1 b 2)
2
> `(a is ,a and b is ,b)
(A IS 1 AND B IS 2)

list를 호출하는 대신에 backquote를 사용하면, 실제로 확장했을 때와 비슷한 모양을 가지도록 매크로를 정의할 수 있다. 예를 들어 nil!은 다음과 같이 정의될 수 있을 것이다:

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

,@은 , 와 비슷하지만 리스트를 인자로 받아서 풀어헤친다는 점에서 다르다. ,@을 붙이면 리스트가 템플릿에 삽입되는 것이 아니라 리스트의 각각의 원소들이 템플릿으로 삽입된다:

> (setf lst '(a b c))
(A B C)
> `(lst is ,lst)
(LST IS (A B C))
> `(its elements are ,@lst)
(ITS ELEMENTS ARE A B C)

,@은 rest parameter를 인자로 가지는 매크로에 유용하다.(예를 들어, 코드의 몸체를 인자로 받는) 테스트 표현식이 참인 동안 몸체 부분을 반복해서 평가하는 while 이라는 매크로를 정의한다고 해보자:

> (let ((x 0))
(while (< x 10)
(princ x)
(incf x)))
0123456789
NIL

while과 같은 매크로를 만들려면 rest parameter를 이용해서 while의 몸체 부분의 표현식들을 리스트로 받은 다음 ,@을 사용해서 리스트를 표현식들로 풀어헤쳐야 한다:

(defmacro while (test &rest body)
'(do ()
((not ,test))
,@body))


10.4 예: 퀵소트

다음 코드는 매크로에 큰 비중을 두고 있는 함수 - 벡터를 퀵소트 알고리즘을 이용해서 정렬하는 - 의 예이다.

퀵소트 알고리즘은 다음과 같다:

1. 일단 pivot이 될 아무 원소나 고른다. 대부분 시퀀스의 중간 정도에 있는 원소를 고르는 경우가 많다.

2. pivot을 경계로 벡터를 나눈다. pivot보다 작은 원소는 모두 pivot의 왼쪽에, pivot보다 같거나 큰 원소는 모두 pivot의 오른쪽에 올 때까지 재배치한다.

3. 나눈 각각의 부분이 2개 이상의 원소로 되어 있다면, 각각의 부분에 대해서 퀵소트 알고리즘을 재귀적으로 수행한다.

알고리즘이 매번 재귀적으로 수행될 때마다, 나뉘어진 부분의 크기가 점점 더 작아지게 되고, 결과적으로 벡터는 정렬된다.

(defun quicksort (vec l r)
(let ((i l)
(j r)
(p (svref vec (round (+ l r) 2)))) ;1
(while (<= i j) ;2
(while (< (svref vec i) p) (incf i))
(while (> (svref vec j) p) (decf j))
(when (<= i j)
(rotatef (svref vec i) (svref vec j))
(incf i)
(decf j)))
(if (> (- j l) 1) (quicksort vec l j)) ;3
(if (> (- r i) 1) (quicksort vec i r)))
vec)

위의 코드에서 함수는 벡터와 두 개의 정수 - 정렬될 범위를 표시하는 - 를 인자로 받는다. 주어진 범위의 가운데에 있는 원소가 pivot p로 정해진다. pivot p를 중심으로 나눠진 파티션 내부가 탐색되면서 p보다 큰 값을 가지는데 p의 왼쪽에 있는 원소와 p보다 작은 값을 가지면서 p의 오른쪽에 있는 원소들이 자리바꿈을 한다. (두 원소를 자리바꿈하기 위해서 rotatef가 사용되었다.) 최종적으로 복수개의 원소를 가지고 있는 파티션이 있다면 그 파티션에 대해서 똑같은 프로세스를 적용한다.

위의 코드에서는 이전 섹션에서 정의했던 while 매크로 외에, 리습에 내장되어 있는 when, incf, decf, rotatef 매크로 등이 사용되었다. 이 같은 매크로의 사용은 코드를 간결하고 명쾌하게 만든다.


10.5 매크로 디자인

매크로를 작성하는 것은 일반적인 프로그래밍과 조금 다르게, 나름의 목표와 문제를 가지고 있다. 매크로를 작성하는 것은 컴파일러가 인식하는 바를 바꾸는 것인데, 이는 어찌보면 컴파일러를 재작성하는 것과도 같다. 따라서 매크로를 작성할 때는 언어 설계자의 입장에서 생각할 필요가 있다.

이 섹션에서는 매크로 작성과 관련된 문제들을 간단히 살펴보고, 그에 대한 해결책을 보이고자 한다. 예를 들어, 숫자 n 을 받아서 몸체 부분을 n 번 평가하는 ntimes 라는 매크로를 만들어보자:

> (ntimes 10
(princ "."))
..........
NIL

다음은 ntimes 매크로의 잘못된 예이다:

(defmacro ntimes (n &rest body) ; 이렇게 하지 말 것!
`(do ((x 0 (+ x 1)))
((>= x ,n))
,@body))

이 코드는 얼핏 보기에 틀린 데가 없어 보인다. 위에서 보인 예와 같이 ntimes 매크로를 사용한다면 문제 없이 작동할 것이다. 하지만 사실 이 코드는 두 가지 문제가 있다.

첫 번째는 매크로를 작성할 때 유의해야 할 점인 '의도하지 않은 변수 캡쳐(inadvertent variable capture)'를 고려하지 않았다는 것이다. 만약에 매크로가 확장되는 부분의 문맥에 매크로에서 사용하는 변수와 똑같은 이름의 변수가 있다면 어떻게 될까? ntimes 매크로에서 잘못된 점은 x 라는 변수를 만든 것이다. 만일 매크로가 불리는 지점에 x라는 이름의 변수가 이미 있다면, 우리가 기대하지 않은 결과를 낳을 것이다.

> (let ((x 10))
(ntimes 5
(setf x (+ x 1)))
x)
10

ntimes가 의도대로 작동한다면, 이 코드는 x를 1씩 다섯 번 증가시킬 것이고 결과적으로 15가 리턴되어야 한다. 하지만 매크로가 x를 이터레이션 변수로 사용하고 있기 때문에 setf 표현식은 우리가 증가시키려고 했던 x가 아니라 이터레이션 변수 x를 증가시키게 된다. 매크로 확장이 일어난 코드는 다음과 같다:

(let ((x 10))
(do ((x 0 (+ x 1)))
((>= x 5))
(setf x (+ x 1)))
x)

생각할 수 있는 방법은 매크로 안에서 변수명으로 어디에서도 사용되지 않을 것 같은 심볼을 사용하는 것이다. 하지만 이런 미봉책 대신에 gensym 이라는 것을 사용할 수 있다. read는 읽는 모든 심볼을 intern시키기 때문에, gensym이 만들어 내는 심볼은 프로그램 내의 어느 심볼과도 같을(eql) 수 없다. gensym을 이용해서 ntimes를 다시 작성한다면, 적어도 '의도하지 않은 변수 캡쳐' 문제는 생기지 않을 것이다:

(defmacro ntimes (n &rest body) ; 하지만 이 코드 역시 문제가 있다.
(let ((g (gensym)))
`(do ((,g 0 (+ ,g 1)))
((>= ,g ,n))
,@body)))

하지만 이 코드 역시 '반복 평가(multiple evaluation)'라는 또 다른 문제를 가지고 있다. 첫번째 인자가 그대로 do 문에 넘겨지기 때문에, do 문의 몸체가 반복될 때마다 n이 새롭게 평가된다. 첫번째 인자로 부효과가 있는 표현식을 넘겨보면 코드에 문제가 있음이 명확하게 드러난다.

> (let ((v 10))
(ntimes (setf v (- v 1))
(princ ".")))
.....
NIL

v가 let에서 10으로 설정되었지만, setf는 두 번째 인자의 값을 리턴하기 때문에 이 코드는 아홉 개의 점을 출력해야 한다. 하지만 실제로 실행시켜 보면 다섯 개의 점만 출력된다.

매크로가 확장된 코드를 보면 왜 이런 결과가 나왔는지 알 수 있다:

(let ((v 10))
(do ((#:g1 0 (+ #:g1 1)))
((>= #:g1 (setf v (- v 1))))
(princ ".")))

종료조건에 해당하는 표현식을 보면 매 반복 주기마다 이터레이션 변수(gensym은 보통 #:가 앞에 붙어 있는 심볼을 출력한다)가 9와 비교되는 것이 아니라, 9에서 1씩 줄어드는 값과 비교되는 것을 알 수 있다. 마치 볼 때마다 점점 다가오는 지평선과도 같다.

이와 같이 의도하지 않게 값을 반복적으로 평가하는 것을 피하기 위해서는, 반복 주기에 들어가기 전에 변수를 만들어 값을 담아두면 된다. 역시 gensym을 사용해서 변수를 생성한다:

(defmacro ntimes (n &rest body)
(let ((g (gensym))
(h (gensym)))
`(let ((,h ,n))
(do ((,g 0 (+ ,g 1)))
((>= ,g ,h))
,@body))))

드디어, 오류없는 ntimes를 만들었다.

의도하지 않은 변수 캡쳐와 반복적인 값의 평가가 매크로를 작성할 때 흔히 범하는 실수이긴 하지만, 그것들이 전부는 아니다. 경험이 쌓이면, 이같은 실수를 피하는 것이 그렇게 어렵지 않다.(숫자를 0으로 나누는 것과 같은 기존의 다른 실수들을 피하는 것보다 어려울 이유가 없다.) 하지만 매크로가 우리에게 새로운 능력을 부여하기 때문에, 우리가 조심해야 하는 문제 역시 새로운 것일 수 밖에 없는 것 뿐이다.

커먼 리습 구현 그 자체가 매크로를 배울 수 있는 좋은 본보기가 될 수 있다. 내장되어 있는 매크로를 펼쳐 봄으로써 그 매크로들이 어떻게 작성되었는지를 이해할 수 있을 것이다. 대부분의 구현에서 cond 표현식을 펼쳐보면 다음과 같다:

> (pprint (macroexpand-1 '(cond (a b)
(c d e)
(t f))))

(IF A
B
(IF C
(PROGN D E)
F))

pprint 함수는 표현식을 들여쓰기 된 코드로 출력하는데, 매크로 확장을 살펴볼 때 사용하면 매우 유용하다.


10.6 일반화된 참조

매크로가 호출되면 호출된 바로 그 자리에서 확장이 일어나기 때문에 매크로 확장의 결과가 setf의 첫번째 인자가 될 수 있다면 그 매크로는 setf의 첫번째 인자로 올 수 있다. 예를 들어, car의 동의어 cah를 정의했다고 하자,

(defmacro cah (lst) `(car ,lst))

car 호출의 결과가 setf의 첫번째 인자로 올 수 있기 때문에 cah 호출 역시 setf의 첫번째 인자로 올 수 있다:

> (let ((x (list 'a 'b 'c)))
(setf (cah x) 44)
x)
(44 B C)

확장한 코드가 setf를 포함하는 매크로는 생각보다 약간 만들기 어려울 수 있다. incf는 다음과 같이 구현할 수 있을 것처럼 보인다:

(defmacro incf (x &optional (y 1)) ; 틀렸다
`(setf ,x (+ ,x ,y)))

이 코드는 제대로 동작하지 않는다. 다음 두 표현식은 동일하지 않다:

(setf (car (push 1 lst)) (1+ (car (push 1 lst))))

(incf (car (push 1 lst)))

lst가 nil이라면, 두 번째 표현식은 (2)가 되지만 첫 번째 표현식은 (1 2)가 된다.

커먼 리습은 setf에 관한 매크로를 작성하기 위한 define-modify-macro 라는 것을 제공한다. define-modify-macro는 세 개의 인자를 받는다: 매크로의 이름, 추가적인 파라미터(변경된 값이 저장될 장소는 암묵적으로 첫번째 인자로 온다고 가정된다), 새로운 값을 만드는데 필요한 함수. define-modify-macro를 이용해서 incf를 다음과 같이 정의할 수 있다:

(define-modify-macro our-incf (&optional (y 1)) +)

그리고 다음은 리스트 끝에 원소를 추가하는 매크로이다

(define-modify-macro append1f (val)
(lambda (lst val) (append lst (list val))))

위의 코드는 다음과 같이 동작한다;

> (let ((lst '(a b c)))
(append1f lst 'd)
lst)
(A B C D)

우연히도, push와 pop 모두 modify-macro로는 정의될 수 없다. push는 값이 저장될 장소가 첫번째 인자로 오지 않기 때문이고, pop은 그 리턴 값이 변경된 객체가 아니기 때문이다.


10.7 예: 매크로 유틸리티

섹션 6.4에서 리습에 내장되어 있는 오퍼레이터들과 같은 일반적인 목적으로 사용될 수 있는 오퍼레이터들을 유틸리티라고 한다고 소개했었다. 함수로 만들기 어려운 유틸리티들을 매크로를 사용해서 만들 수 있다. 우리는 이미 몇 가지 예를 보았다: nil!, ntimes, while 등은 함수가 아니라 매크로로만 작성될 수 있는데, 함수는 그 인자들을 모두 평가하는 반면, nil!, ntimes, while은 모두 그 인자들이 평가되는 방식을 조절할 필요가 있기 때문이다. 이 섹션에서는 매크로로 작성할 수 있는 유틸리티들의 예를 더 살펴보려고 한다. 다음은 실제로 쓸모가 있는 유틸리티 매크로들이다:

(defmacro for (var start stop &body body)
(let ((gstop (gensym)))
`(do ((,var ,start (1+ ,var))
(,gstop ,stop))
((> ,var ,gstop))
,@body)))

(defmacro in (obj &rest choices)
(let ((insym (gensym)))
`(let ((,insym ,obj))
(or ,@(mapcar #'(lambda (c) '(eql ,insym ,c))
choices)))))

(defmacro random-choice (&rest exprs)
`(case (random ,(length exprs))
,@(let ((key -1))
(mapcar #'(lambda (expr)
`(,(incf key) ,expr))
exprs))))

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

(defmacro with-gensyms (syms &body body)
`(let ,(mapcar #'(lambda (s)
'(,s (gensym)))
syms)
,@body))

(defmacro aif (test then &optional else)
`(let ((it ,test))
(if it ,then ,else)))

먼저, for는 while과 유사한 매크로이다. 반복 주기마다 변수가 범위 안의 값에 차례대로 바인딩 되면서 몸체 안의 값이 평가된다:

> (for x 1 8
(princ x))
12345678
NIL

같은 일을 do로 하는 것보다 쉽다,

(do ((x 1 (1+ x)))
((> x 8))
(princ x))

이 do 코드는 for 매크로를 확장한 결과와 거의 똑같다:

(do ((x 1 (1+ x))
(#:g1 8))
((> x #:g1))
(princ x))

for 매크로는 범위의 마지막 값을 저장하기 위해서 새로운 변수를 만들 필요가 있다. 위의 예에서 8이라는 값이 한 번 주어지면 우리는 그 값이 반복적으로 평가되는 것을 원하지 않는다. 따라서 의도하지 않은 변수 캡쳐를 피하기 위해 gensym에 의해 새로운 변수가 도입되었음을 볼 수 있다.

두 번째 매크로 in은 첫번째 인자가 나머지 인자 중 어느 하나와 같으면 참을 리턴하는 매크로이다. 매크로를 이용하면 다음과 같이 표현할 수 있다:

(in (car expr) '+ '- '*)

in 매크로가 없다면 같은 일을 하기 위해 다음과 같이 코드를 작성해야 것이다:

(let ((op (car expr)))
(or (eql op '+)
(eql op '-)
(eql op '*)))

in 매크로가 확장되면 op가 gensym에 의해 생성된 변수로 바뀌는 것 빼고는 위와 동일한 코드가 된다.

다음으로, random-choice는 평가할 인자를 랜덤으로 선택한다. 우리는 74 페이지에서 두 대안 중 하나를 랜덤으로 선택할 필요에 맞닥뜨린 적이 있었다. random-choice 매크로는 이와 같은 상황에 대해 일반적인 해결책을 제시한다. 다음과 같은 호출은

(random-choice (turn-left) (turn-right))

이와 같이 확장된다:

(case (random 2)
(0 (turn-left))
(1 (turn-right)))

다음으로, with-gensyms는 매크로의 몸체 부분을 작성하는 데 도움을 주기 위한 매크로이다. 매크로를 작성하다 보면 gensym을 이용해서 여러 개의 변수들을 생성할 일이 종종 있다. with-gensyms를 이용하면 다음과 같이 작성하는 대신에

(let ((x (gensym)) (y (gensym)) (z (gensym)))
...)

다음과 같이 쓸 수 있다

(with-gensyms (x y z)
...)

지금까지 보아온 매크로들은 모두 함수로는 대신할 수 없다. 원칙적으로, 매크로를 작성하는 이유는 함수로 그것을 할 수 없기 때문이다. 하지만 여기에도 몇 가지 예외가 있다. 어떤 일을 런타임이 아니라 컴파일 타임에 하기 원할 때 그것을 매크로로 작성할 수 있다. 다음 매크로 avg는 인자들의 평균을 리턴한다,

> (avg 2 4 8)
14/3

이 매크로가 앞에서 말한 예외의 예가 될 수 있다. avg를 다음과 같이 함수로 작성할 수도 있는데,

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

하지만 이 함수는 숫자들의 개수를 런타임 시에 구한다. 우리가 avg를 apply에 넘길 것만 아니라면 avg내의 length를 컴파일 타임에 호출하지 않을 이유가 없다.

마지막으로 aif는 의도적인 변수 캡쳐의 예로 포함된 것이다. aif 매크로는 테스트 인자의 반환 값을 변수 it을 사용해서 참조할 수 있도록 한다. 즉, 다음과 같이 표현하는 대신에

(let ((val (calculate-something)))
(if val
(1+ val)
0))

다음과 같이 쓸 수 있다.

(aif (calculate-something)
(1+ it)
0)

잘만 사용한다면 의도적 변수 캡쳐는 유용한 테크닉일 수 있다. 이런 테크닉이 커먼 리습 언어 자체에서도 쓰인 것을 종종 발견할 있다. next-method-p와 call-next-method 등이 의도적인 변수 캡쳐의 예이다.

지금까지 본 매크로들을 통해 프로그램을 작성하는 프로그램이 어떤 것인지 알 수 있을 것이다. 한번 for를 정의해 놓으면 do 문을 길게 풀어 쓰는 수고를 들일 필요가 없다. 단지 타이핑을 줄여주는 것 정도인 것 같은데 그게 과연 가치 있는 일일까? 매우 가치가 있다. 타이핑을 줄여주는 것이 프로그래밍 언어가 하는 일의 전부이다; 컴파일러의 목적은 당신이 프로그램을 기계어로 타이핑하지 않아도 되게 해주는 것이다. 그리고 매크로는 하이레벨 언어로 일반적인 프로그래밍을 할 때 얻을 수 있는 것과 비슷한 이득을 당신이 짜고자 하는 특정한 프로그램에 가져다준다. 매크로를 주의 깊게 사용한다면 당신의 프로그램을 놀랄 정도로 짧게 만들 수 있고 또한 읽고 쓰고 유지하기 쉽게 만들 수 있다.

이런 내용이 믿어지지 않는다면, 언어에 내장된 매크로를 전혀 쓰지 않고 프로그램을 작성할 때 어떤 일이 벌어질지 상상해보라. 매크로들이 확장해 내는 그 코드들을 당신이 스스로 손으로 작성해야 할 것이다. 이 같은 사고 방식을 더욱 확장해서 적용할 수 있다. 프로그램을 짤 때, 매크로가 확장해야 할 부분을 손으로 일일이 타이핑하고 있는 것은 아닌지 스스로에게 물어보라. 만일 그렇다면, 그 코드를 타이핑할 것이 아니라 그런 코드를 만들어내는 매크로를 작성해야 할 때인 것이다.


10.8 On Lisp

이제 매크로가 무엇인지 알았으니, 리습으로 리습 언어 자체를 더욱 확장해 나갈 수 있게 되었다. 함수가 아닌 대부분의 커먼 리습 오퍼레이터들은 매크로이고 그것들은 모두 리습으로 작성된 것이다. 커먼 리습의 내장 특수 오퍼레이터는 25개 뿐이다.

John Foderaro는 리습을 "프로그래밍 가능한 프로그래밍 언어"라고 표현했다. 자신만의 함수와 매크로를 작성함으로써 리습을 원하는 어떤 언어로도 만들 수 있다. (우리는 17장에서 이와 같은 실제 예를 보일 것이다.) 당신이 원하는 프로그램을 짜기에 적합한 언어의 모습이 어떤 것이든지, 리습은 그와 같이 변할 수 있다.

매크로는 이와 같은 유연성을 가능하게 하는 핵심이다. 매크로는 리습을 당신이 상상하지 못하는 데까지 변형시킬 수 있는 가능성을 제공한다. 그리고 이 모든 것들은 잘 정의된 규칙에 따라, 프로그램의 수행 성능을 저하시키지 않고 이루어진다. 리습 커뮤니티에서 매크로에 대한 관심은 계속해서 높아지고 있다. 매크로로 이미 많은 놀라운 일들을 할 수 있다는 것은 명백하지만, 매크로에 대해 연구해야 할 여지는 아직도 많이 남아 있다. 리습은 프로그래머들의 손에 의해 끊임없이 진화해 왔고, 원한다면 당신 역시 그렇게 할 수 있다. 그것이 바로 리습이 지금까지 살아남은 이유이다.


translated by 찬우

2. 웰컴 투 리습 - ANSI Common Lisp 번역

2. 웰컴 투 리습

이 장의 목표는 당신으로 하여금 최대한 빨리 프로그래밍을 시작할 수 있게 하는 것이다. 이 장을 다 읽게 되면 바로 리습으로 프로그램을 작성할 수 있을 정도의 지식을 갖추게 될 것이다.


2.1 폼(Form)

리습은 인터랙티브한 언어이기 때문에, 리습을 사용해 봄으로써 리습에 대해서 배울 수 있다. 어떤 리습 시스템이던지 탑레벨(toplevel)이라 불리는 상호작용 가능한 프론트엔드를 가지고 있다. 탑레벨에 리습 표현(expression)을 치게 되면, 시스템은 그 결과를 표시하게 된다.

리습은 프롬프트를 표시해서 사용자의 입력을 기다리고 있음을 표시한다. 많은 커먼 리습 구현들은 '>'를 사용해서 탑레벨 프롬프트를 표시한다. 이 책에서도 '>'를 사용해서 프롬프트를 표시하겠다.

리습의 가장 간단한 표현 중 하나는 integer이다. 우리가 프롬프트에 1을 치면

> 1
1
>

시스템은 그 값을 표시한 후에, 새로운 프롬프트를 표시하여 또 다른 입력을 기다리고 있음을 알린다.

이 경우에는 우리가 친 값과 표시되는 값이 같다. 1과 같은 숫자는 평가의 결과가 그 자신이 된다. 이것보다는 뭔가 더 평가할 여지가 있는 것을 입력해보면 재밌을 것이다. 예를 들어, 우리가 두 개의 숫자를 더하고 싶다고 하자, 우리는 다음과 같이 칠 수 있다:

> (+ 2 3)
5

(+ 2 3) 이라는 표현에서 +는 오퍼레이터(operator)라고 불리며, 2와 3은 인자(argument)라고 불린다.

우리는 일상에서는 이 같은 표현을 2+3 과 같이 쓴다. 하지만 리습에서는 + 오퍼레이터가 맨 처음에 오고 그 뒤에 인자들이 따라오고, 전체 표현식으 괄호에 의해 둘러싸여진다: (+ 2 3). 이런 표시 방식을 오퍼레이터가 맨 앞에 오기 때문에 전위 표기 방식(prefix notation)이라고 한다. 이런 방식은 처음 보면 이상해 보이지만, 사실 이 표기 방식은 리습의 가장 뛰어난 면 중 하나이다.

예를 들어, 우리가 세 개의 숫자를 더하려고 한다고 하자. 일반적으로 우리는 + 를 두 번 사용해야 한다.

2 + 3 + 4

리습에서는 그저 하나의 인자를 추가할 뿐이다.

(+ 2 3 4)

우리가 일상적으로 + 를 사용하는 방식에서는, + 좌우에 정확히 두 개의 인자만이 와야 한다. 리습에서 사용하는 전위 표기 방식은 + 오퍼레이터가 몇 개의 인자든(인자가 없는 경우도 포함해서) 받을 수 있도록 유연성을 제공한다.

> (+)
0
> (+ 2)
2
> (+ 2 3)
5
> (+ 2 3 4)
9
> (+ 2 3 4 5)
14

오퍼레이터가 받을 수 있는 인자의 개수에 제한이 없기 때문에 우리는 괄호를 사용해서 표현식이 시작하고 끝나는 것을 표시할 필요가 있다.

표현식은 중첩될 수 있다. 다시 말하면, 표현식의 인자 자체가 또 하나의 복잡한 표현식이 될 수도 있다는 것이다.

> (/ (- 7 1) (- 4 2))
3

한글로 말하면 '칠 빼기 일'을 '사 빼기 이'로 나눈 값이 된다.

리습의 또 다른 아름다운 점은 모든 리습 표현식은 아톰(atom)이거나 리스트(list) 둘 중에 하나라는 것이다. 다음은 모두 문법에 맞는 리습 표현식이다:

2 (+ 2 3) (+ 2 3 4) (/ (- 7 1) (- 4 2))

앞으로 보게 되겠지만, 모든 리습 코드는 이같은 형태를 띤다. C같은 언어는 보다 복잡한 형식을 가지고 있다: 수식 표현은 중위 표기(infix) 방식을 사용하고; 함수 호출은 콤마로 구분된 인자와 함께 전위 표기 방식의 일종을 사용한다; 표현식은 세미콜론으로 끝나야 하며, 코드의 블록은 중괄호 { } 를 사용해서 표시한다. 리습은 단 하나의 표현 방법으로 이 모든 것들을 표현할 수 있다.


2.2 평가(Evaluation)

이전 섹션에서, 우리는 탑레벨에서 표현을 입력했고, 리습은 그 값을 표시했다. 이 섹션에서 우리는 리습이 표현식을 어떻게 평가하는지를 좀 더 자세히 살펴볼 것이다.

리습에서 + 는 함수이다. 그리고 (+ 2 3)과 같은 표현식은 함수의 호출이 된다. 리습이 함수의 호출을 평가하는 것은 두 단계로 이루어진다:

1. 먼저 인자들이 왼쪽에서 오른쪽 방향으로 차례대로 평가된다. 이 경우에는 각 인자들을 평가하면 그 자신이 되기 때문에 각 인자의 값은 차례대로 2와 3이 된다.

2. 각 인자의 값이 오퍼레이터가 나타내는 함수로 넘겨진다. 이 경우에는 + 함수로 인자가 넘겨지고 함수는 5를 리턴한다.

만일 인자 중 하나가 함수 호출이라면, 그것들 역시 똑같은 방식으로 평가된다. 따라서 (/ (- 7 1) (- 4 2)) 표현식의 평가는 다음과 같이 이루어진다:

1. (- 7 1)이 평가된다: 7과 1은 각각 7과 1로 평가된다. 이 값들이 - 함수로 넘겨지고 함수는 6을 리턴한다.

2. (- 4 2)가 평가된다: 4와 2는 각각 4와 2로 평가된다. 이 값들이 - 함수로 넘겨지고 함수는 2를 리턴한다.

3. 6과 2가 / 함수로 넘겨지고 함수는 3을 리턴한다.

커먼 리습에 존재하는 오퍼레이터(operator)가 모두 함수(functions)인 것은 아니지만, 대부분은 함수이다. 그리고 함수 호출은 모두 이 같은 방식으로 평가된다. 인자들은 왼쪽에서 오른쪽으로 차례대로 평가되고, 그 값들이 함수에게 넘겨지면, 함수는 전체 표현식의 값을 리턴하게 된다. 이것이 커먼 리습에서의 평가 규칙(evaluation rule)이다.


곤란에서 벗어나기

만일 당신이 리습이 이해할 수 없는 것을 입력했다면, 에러 표시가 나타나면서 break loop라고 불리는 탑레벨의 일종으로 떨어지게 될 것이다. break loop는 리습 프로그래머가 무엇이 에러를 발생시켰는지 알아볼 수 있도록 도와주는 것이지만, 일단 지금으로서는 break loop에서 빠져나오는 방법만 알면 될 것이다. 어떻게 해야 다시 탑레벨로 돌아올 수 있는지는 사용하는 리습 구현에 따라 조금씩 다르다. 다음의 가상적인 리습 구현에서는 :abort 가 break loop 로부터 빠져나오게 해준다. (역주: 저같은 경우에는 q를 치면 디버거에서 빠져 나오더군요)

> (/ 1 0)
Error: Division by zero.
Options: :abort, :backtrace
>> :abort
>

부록 A 에서는 전형적인 에러들의 예를 들고, 어떻게 리습 프로그램을 디버그하는지를 설명하고 있다.


커먼 리습의 평가 규칙을 따르지 않는 오퍼레이터로는 인용부호(quote)가 있다. 인용부호 오퍼레이터는 자신만의 고유한 평가 규칙을 갖는 특수 오퍼레이터(special operator)이다. 그리고 그 룰은 '아무 처리도 하지 말라'이다. 인용부호 오퍼레이터는 하나의 인자를 받아서 그것을 쓰여진 문자 그대로 리턴한다.

> (quote (+ 3 5))
(+ 3 5)

편의를 위해서, 커먼 리습에서는 ' 기호를 quote의 약어로 사용할 수 있도록 하고 있다. 어떤 표현식 앞에 ' 기호를 붙임으로써 quote를 호출하는 것과 똑같은 일을 할 수 있다.
> '(+ 3 5)
(+ 3 5)

' 기호를 써서 약어로 표시하는 것이 quote를 써서 표시하는 것에 비해 훨씬 일반적이다.

quote는 표현식이 평가되지 않게 보호하는 역할을 한다. 다음 섹션에서 이 같은 보호가 어떤 쓸모가 있는지 알아볼 것이다.


2.3 데이터

리습은 우리가 다른 언어에서 찾아볼 수 있는 모든 종류의 데이터 타입과 함께, 다른 언어에서 찾아볼 수 없는 데이터 타입들도 제공한다. 먼저, 우리가 이미 사용했던 데이터 타입으로 정수형(숫자들의 연속으로 이루어진)이 있다: 256. 또 다른 데이터 타입으로는 다른 언어들과 비슷하게 겹따옴표로 둘러싸서 표현할 수 있는 문자열이 있다: "ora et labora". 정수형과 문자열이 평가되면 그대로 그들 자신이 된다.

다른 언어에서 찾기 어려운 리습만의 데이터 타입으로는 심볼(Symbol)과 리스트(list)가 있다. 심볼은 그저 문자 그대로의 단어들을 말한다. 일반적으로 심볼은 어떻게 치던간에 모두 대문자로 바뀌어서 나타나게 된다.

> 'Artichoke
ARTICHOKE

심볼은 대체로 평가되지 않고 그대로 쓰이는 경우가 대부분이다. 따라서 심볼을 참조하고 싶다면 위에서 보는 것처럼 quote를 붙여야 한다.

리스트는 0 개 이상의 원소들을 괄호로 둘러싼 것으로 표현된다. 각각의 원소들은 어떤 데이터 타입도 될 수 있으며, 리스트도 될 수 있다. 리스트에는 quote를 붙여야 하는데, 그렇지 않으면 리습은 그 리스트를 함수 호출로 볼 것이기 때문이다:

> '(my 3 "Sons")
(MY 3 "Sons")
> '(the list (a b c) has 3 elements)
(THE LIST (A B C) HAS 3 ELEMENTS)

하나의 quote가 표현식 전체와 그 표현식 안의 표현식까지도 보호하고 있다는 점에 주목하라.

list 를 호출해서 리스트를 만들 수도 있다. list 는 함수이기 때문에 그 인자들은 평가된다. 다음 예를 보자:

> (list 'my (+ 2 1) "Sons")
(MY 3 "Sons")

여기서 우리는 리습을 그토록 특별하게 만드는 것이 무엇인지 볼 수 있다. 그것은 바로 리습 프로그램은 리스트로 표현된다는 것이다. 인자를 취하는 데 있어서의 유연성과 우아함은 당신으로 하여금 리습 표기 방식의 뛰어남을 충분히 납득시키지 못할 수 있다. 하지만 리습 프로그램이 리습의 데이터 타입 중 하나인 리스트로 표현될 수 있다는 것은 리습의 표기 방식이 얼마나 가치 있는 것인가를 보여준다. 그것은 즉, 리습 프로그램이 리습 코드를 만들어 낼 수 있다는 것이다. 리습 프로그래머는 프로그램을 짜는 프로그램을 만들어 낼 수 있다. 그리고 그들은 종종 그렇게 한다.

그와 같은 프로그램은 10장에 가서야 살펴볼 것이지만, 혼란을 피하기 위해서 표현식과 리스트 사이의 관계를 확실히 이해할 필요가 있다. 만일 리스트에 quote가 붙어 있다면, 그 리스트는 그대로 리턴된다; 만일 quote가 붙어 있지 않다면, 그 리스트는 코드로 취급되고, 평가한 값이 리턴된다.

> (list '(+ 2 1) (+ 2 1))
((+ 2 1) 3)

여기서 첫 번째 인자는 quote가 붙어 있기 때문에 리스트가 된다. 두 번째 인자는 quote가 붙어 있지 않기 때문에 함수 호출로 취급되고 그 결과는 숫자가 된다.

커먼 리습에서 빈 리스트를 표현하는 방법은 두 가지가 있다. 빈 괄호로 표시하거나 nil 심볼을 사용하는 것이다. 어떤 식으로 표현하는가는 문제가 되지 않는다. 두 경우 모두 nil 로 표시된다:

> ()
NIL
> nil
NIL

nil을 평가하면 그 자신이 되기 때문에 nil 에 quote를 붙일 필요는 없다.(붙인다고 해도 문제가 되진 않겠지만)


2.4 리스트 연산

cons 함수는 리스트를 만든다. 두 번째 인자가 리스트이면, 첫 번째 인자를 맨 앞에 붙여서 만든 새로운 리스트를 리턴한다:

> (cons 'a ' (b c d))
(A B C D)

빈 리스트에 cons를 사용해서 새로운 원소를 추가함으로써 리스트를 만들 수도 있다. 앞 섹션에서 본 list 함수는 cons를 사용하여 여러 개의 원소를 nil에 추가함으로써 리스트를 만드는 방법의 편리 버전일 뿐이다:

> (cons 'a (consb nil))
(A B)
> (list ;a >b)
(A B)

car와 cdr은 리스트에서 원소를 얻어내는 함수이다. car는 첫 번째 원소를 얻어내고, cdr은 첫 번째 원소를 제외한 나머지 전부를 얻어낸다:

> (car '(a b c))
A
> (cdr '(a b c))
(B C)

리스트의 특정 원소를 얻어내기 위해서는 car와 cdr의 조합을 사용할 수 있다. 만일 세 번째 원소를 얻어내고 싶다면, 다음과 같이 하면 된다:

> (car (cdr (cdr '(a b c d))))
C

third를 사용하면 더욱 쉽게 할 수 있다:

> (third '(a b c d))
C


2.5 참과 거짓

커먼 리습에서는 심볼 t가 참을 나타내는 기본 표현이다. nil처럼 t는 평가하면 자기 자신이 된다. listp 함수는 인자가 리스트이면 참을 리턴한다.

> (listp '(a b c))
T

참이나 거짓을 리턴하도록 되어 있는 함수를 predicate이라고 한다. 커먼 리습의 predicate들은 대체로 p로 끝나는 이름을 가지고 있다.

커먼 리습에서의 거짓은 nil 또는 빈 리스트로 표현된다. listp에 리스트가 아닌 것을 인자로 주면 listp는 nil을 리턴한다:

> (listp 27)
NIL

nil은 거짓을 표현하기도 하고 빈 리스트를 표현하기도 하기 때문에, 빈 리스트를 인자로 주면 참을 리턴하는 함수 null과,

> (null nil)
T

거짓 값을 인자로 주면 참을 리턴하는 함수 not은,

> (not nil)
T

같은 결과를 보인다.

커먼 리습의 가장 간단한 조건문을 if 이다. if는 일반적으로 세 개의 인자를 받는다: test 표현식, then 표현식, else 표현식. test 표현식이 평가된 결과가 참이면, then 표현식이 평가되고 그 값이 리턴된다. test 표현식을 평가한 결과가 거짓이면, else 표현식이 평가되고 그 값이 리턴된다:

> (if (listp '(a b c))
(+ 1 2)
(+ 5 6))
3
> (if (listp 27)
(+ 1 2)
(+ 5 6))
11

quote처럼, if 역시 특수 오퍼레이터(special operator)이다. if 는 함수로 구현될 수가 없는데, 함수는 호출시에 그 인자들이 모두 평가되는 반면에, if 문의 경우에는 마지막 두 개의 표현식 중에 하나만 평가되어야 하기 때문이다.

if 의 마지막 인자는 생략할 수 있다. 생략되면, 마지막 인자는 nil이 된다:

> (if (listp 27)
(+ 2 3))
NIL

t 가 참을 나타내는 기본 표현이긴 하지만, nil이 아닌 모든 것은 논리적으로 참으로 취급된다:

> (if 27 1 2)
1

논리 오퍼레이터 and 와 or 는 인자를 개수의 제한 없이 받아서, 참과 거짓을 판별하는 데 필요한 만큼만 평가한다. 만일 모든 인자가 참이라면(즉, nil이 아니라면), and는 마지막 인자의 값을 리턴한다:

> (and t (+ 1 2))
3

하지만 어떤 인자가 거짓으로 판명되면, 그 이후의 인자들은 평가되지 않는다. or도 유사한데, 어떤 인자가 참으로 판명되면 거기서 평가를 멈춘다.

and와 or, 이 두 가지 오퍼레이터는 매크로(macro)이다. 특수 오퍼레이터(special operator)와 같이 매크로도 일반적인 평가 규칙을 따르지 않을 수 있다. 10장에서 어떻게 매크로를 작성하는지에 대해서 설명할 것이다.


2.6 함수

새로운 함수를 defun 을 통해 정의할 수 있다. defun은 일반적으로 세네개의 인자를 받는다: 함수 이름, 인자들의 리스트, 함수의 몸체가 될 하나 이상의 표현식. 예를 들어 third 함수는 다음과 같이 정의될 수 있을 것이다:

> (defun our-third (x)
(car (cdr (cdr x))))
OUR-THIRD

첫 번째 인자는 이 함수의 이름이 our-third라는 것을 나타낸다. 두 번째 인자는 리스트 (x)인데, 이 함수가 정확히 하나의 인자 x 를 받는다는 것을 나타낸다. 값을 저장하기 위한 심볼을 변수(variable)라고 하는데, 여기서 사용된 x 와 같이 변수가 함수의 인자를 표현하는데 사용되면 매개 변수(parameter)라고 한다.

정의의 나머지 부분인 (car (cdr (cdr x))) 는 함수의 몸체(body)부분으로 함수의 값을 리턴하기 위해 무엇을 계산해야 할지 말해준다. 따라서 우리가 x 에 어떤 것을 인자로 주던지 our-third 함수는 (car (cdr (cdr x)))를 리턴할 것이다.

> (our-third i (a b c d))
C

이제 변수가 무엇인지 알았으니, 심볼이 무엇인지 이해하는 것은 어렵지 않다. 심볼은 변수의 이름이면서, 또한 그 자신만의 권리를 갖고 있는 객체이다. (역주: 커먼 리습에는 Common Lisp Object System이 포함되어 있어서 객체지향 프로그램을 기본적으로 지원하지만, 특별한 언급 없이 일반적으로 언급되는 객체는 클래스의 인스턴스로서의 객체가 아니라 그저 하나의 object를 말한다. 여기서와 이 장 전체에서 객체라는 말은 모두 하나의 object를 가리키는 표현으로 사용되었다.) 리스트와 마찬가지로, 심볼도 quote를 붙여야 한다. 리스트에 quote를 붙이지 않으면 그 리스트는 코드로 다뤄질 것이다; 심볼에 quote를 붙이지 않으면 그 심볼은 변수로 취급될 것이다.

리습 표현식의 일반화된 버전이 함수라고 생각할 수 있다. 다음 표현식은 1과 4의 합이 3보다 큰지를 검사한다:

> (> (+ 1 4) 3)
T

특정한 숫자를 변수로 바꿈으로써, 두 숫자의 합이 세 번째 숫자보다 큰지를 검사하는 함수를 만들 수 있다.

> (defun sum-greater (x y z)
(> (+ x y) z))
SUM-GREATER
> (sum-greater 1 4 3)
T

리습에서는 프로그램, 프로시져(procedure), 함수의 구별이 없다. 함수는 모든 것을 처리할 수 있다.(함수는 리습 언어의 대부분을 구성하고 있다.) 만일 당신이 특정한 하나의 함수를 main 함수로 삼고 싶다면 물론 그렇게 할 수 있다. 하지만 탑레벨에서 어떤 함수든 호출하는 것이 가능한데, 이것은 프로그램을 함수 단위로 작성해 나가면서 그것들을 바로바로 테스트 할 수 있다는 것을 의미한다.


2.7 재귀

이전 섹션에서 우리가 정의한 함수들은 함수 내에서 다른 함수를 호출해서 자신이 하려는 일을 거들게 했다. 예를 들어, sum-greater 함수는 + 함수와 > 함수를 호출했다. 함수는 어떤 함수든 호출할 수 있다. 그 자신까지도.

자기 스스로를 호출하는 함수를 재귀적(recursive)이라고 한다. 커먼 리습의 member 라는 함수는 어떤 객체가 리스트의 원소인지 아닌지를 검사한다. member를 재귀적으로 정의한 간단 버전은 다음과 같다:

(defun our-member (obj lst)
(if (null lst)
nil
(if (eql (car lst) obj)
lst
(our-member obj (cdr lst)))))

predicate eql 은 두 인자가 같은지를 검사한다; 코드에서 eql 외에 모든 것은 우리가 이미 아는 것이다. our-member 를 실제로 호출해보자:

> (our-member 'b '(a b c))
(B C)
> (our-member 'z '(a b c))
NIL

our-member의 정의를 말로 풀어보면 다음과 같다. 객체 obj 가 리스트 lst 의 멤버인지를 검사하기 위해,

1. 먼저 lst가 비어있는지를 검사한다. 만일 비어있다면, obj는 lst의 멤버가 아니며, nil이 리턴된다.
2. lst가 비어있지 않고, obj가 lst의 첫 번째 원소라면, obj는 lst의 멤버라고 할 수 있다.
3. 위의 경우가 둘 다 아니라면, obj가 lst의 멤버이기 위해서는 obj는 lst의 첫 번째 원소를 제외한 나머지의 멤버여야만 한다.

재귀적 함수가 어떤 식으로 동작하는지를 알기 위해서는, 위와 같이 말로 풀어보는 것이 도움이 된다.

많은 사람들이 처음에는 재귀를 이해하기 어려운 것이라고 생각한다. 이런 어려움은 대부분 함수에 대한 잘못된 비유에서 생겨나는 것이다. 사람들은 함수를 일종의 기계처럼 생각하는 경향이 있다. 원재료가 인자로서 기계에 넘겨지면; 그 중 몇몇 일들은 다른 함수에게 맡겨지고; 그렇게 해서 만들어진 각각의 부품이 조립되어 리턴 값으로 선적된다는 식으로 생각하는 것이다. 우리가 함수에 대해서 이런 식으로 생각한다면, 재귀는 패러독스로 다가올 수밖에 없다. 기계가 자기 스스로에게 일을 맡긴다는 게 무슨 의미가 있는가? 그렇다고 일이 덜어질 것도 아닌데 말이다.

이보다 나은 비유는 함수를 일종의 프로세스로 생각하는 것이다. 무엇인가를 진행하는 프로세스에서 재귀는 자연스러운 것이다. 우리는 매일 재귀적인 프로세스를 보면서 살아간다. 예를 들어, 유럽의 인구 변화에 관심이 있는 역사가가 있다고 하자. 관련된 문서를 찾아보는 프로세스는 다음과 같다:

1. 어떤 문서를 얻는다.

2. 인구 변화에 관련된 정보가 있는지 살펴본다.

3. 만일 그 문서가 다른 유용한 문서를 언급하고 있다면 그 문서를 찾아본다.

이 프로세스는 재귀적이지만 이해하기에 아무 어려움이 없다. 이 프로세스가 재귀적인 이유는 세 번째 단계에 이르러 다시 전체 프로세스를 반복할 수 있기 때문이다.

our-member를 어떤 것이 리스트의 멤버인지를 테스트하는 기계로 생각하지 말자. 대신에 어떤 규칙으로 생각하자. 우리가 함수를 이런 식으로 생각한다면, 재귀가 패러독스처럼 느껴지지 않을 것이다.


2.8 리습 코드 읽기

앞 섹션에서 정의했던 our-member 코드는 다섯개의 괄호를 닫으며 끝난다. 더 복잡한 함수의 정의는 더 많은 개수의 괄호로 끝날 수도 있다. 리습을 처음 배우는 사람들은 수많은 괄호들을 보고 좌절하게 된다. 어떻게 이렇게 많은 괄호들을 구별하며 코드를 읽고, 또 쓸 수가 있을까? 이 괄호가 어떤 괄호와 쌍을 이루는지 어떻게 알까?

답은, 알 필요가 없다는 것이다. 리습 프로그래머들은 괄호가 아니라 들여쓰기를 보고 코드를 읽고 쓴다. 코드를 작성할 때는 텍스트 편집기가 괄호의 쌍을 체크해준다. 리습 코딩을 하기 위한 텍스트 편집기는 반드시 괄호의 쌍을 체크하는 기능을 가지고 있어야 한다. 만일 사용하는 편집기가 괄호 매칭 기능이 없다면, 코딩을 중단하고 방법을 찾아야 한다. 괄호 매칭 기능 없는 편집기로 리습 코딩을 한다는 것은 불가능하기 때문이다.

적당한 에디터를 사용한다면, 코딩할 때 괄호를 매칭하는 것은 더 이상 문제가 되지 않는다. 그리고 리습 코드의 들여쓰기 역시 일정하게 정해져 있기 때문에 코드를 읽는 것 역시 어렵지 않다. 모두가 똑같은 들여쓰기를 하기 때문에, 괄호를 신경쓰지 않고 들여쓰기만으로 코드를 읽을 수 있다.

아무리 숙련된 리습 해커라 하더라도 our-member의 코드가 다음과 같이 되어 있다면 읽기 어려울 것이다:

(defun our-member (obj lst) (if (null lst) nil (if
(eql (car lst) obj) lst (our-member obj (cdr lst)))))

대신에 코드가 적절하게 들여쓰기 되어있다면, 어려울 것이 없다. 대부분의 괄호를 생략한다 해도 여전히 코드를 읽을 수 있다:

defun our-member (obj lst)
if null lst
nil
if eql (car lst) obj
lst
our-member obj (cdr lst)

종이에 코드를 적을 때는 이렇게 들여쓰기만 지키면서 괄호를 생략하는 방식도 유용하다. 그러다 에디터로 코드를 옮겨 적게 되면, 에디터의 기능에 힘입어 괄호 매칭은 쉽게 할 수 있다.


2.9 입력과 출력

여태까지는 탑레벨에서 입출력 비슷하게 해 왔지만, 실제 프로그램에서는 이것으로 충분하지 않다. 이 섹션에서는 입출력과 관계된 함수들을 살펴보려고 한다.

커먼 리습의 대표적인 출력 함수는 format 이다. format은 두 개 이상의 인자를 받는다: 첫 번째 인자는 어디에 출력할 것인지를 가리키고, 두 번째 인자는 문자열 템플릿이 되며, 나머지 인자들은 템플릿 속에 들어갈 표현들이 된다. 예를 들면:

> (format t "~A plus ~A equals ~A. ~%" 2 3 (+ 2 3))
2 plus 3 equals 5.
NIL

결과는 두 행에 걸쳐 나타난다. 첫 번째 행은 format이 출력한 것이다. 두 번째 행은 format을 호출함에 따라 리턴된 값이 표시된 것이다. 일반적으로 format 같은 함수는 탑레벨에서 불리기보다 프로그램 안에서 호출된다. 따라서 format의 리턴 값을 볼 일은 별로 없다.

format의 첫 번째 인자인 t는 출력의 방향을 기본 출력으로 하라는 것을 나타낸다. 일반적으로 기본 출력은 탑레벨로 지정되어 있다. 두 번째 인자는 출력을 위한 템플릿으로 기능하는 문자열이다. 문자열 안의 ~A는 채워져야 할 위치들을 나타내고, ~%는 새로운 행으로 개행하라는 것을 나타낸다. ~A로 표시되는 위치에 나머지 인자들의 값이 차례대로 채워지게 된다.

입력을 위한 대표적인 함수로 read 가 있다. 아무 인자도 주어지지 않으면 read는 일반적으로는 탑레벨이 되는 기본 위치에서 입력값을 읽어온다. 예를 들어, 사용자의 입력을 받아서 그대로 출력하는 다음과 같은 함수가 있다고 하자:


(defun askem (string)
(format t "~A" string)
(read))

이 함수를 호출한 결과는 다음과 같다:

> (askem "How old are you? ")
How old are you? 29
29

read는 사용자가 타이핑하고 엔터를 칠 때까지 계속해서 기다린다. 따라서 명시적으로 프롬프트를 출력하지 않은 채로 read를 호출해서 계속 입력을 기다리는 것은 프로그램이 멈췄다는 오해를 불러 일으킬 여지가 있다.

read에 관해 또 하나 알아야 할 것은 read는 굉장히 강력한 도구라는 것이다: read는 완전한 하나의 리습 파서(parser)이다. 단지 문자들을 읽어서 문자열로 리턴하기만 하는 것이 아니다. read는 읽어들인 내용을 파싱해서 리습 객체를 결과로 리턴한다. 위의 예에서는 숫자를 리턴한 것이다.

askem 코드를 보면 함수의 몸체가 여러 개의 표현식으로 구성되어 있는데, 이는 이전에 살펴본 함수들에서는 보지 못했던 것이다. 함수의 몸체는 몇 개의 표현식이든 가질 수 있다. 함수가 호출되면 각 표현식이 차례대로 평가되고, 맨 마지막 표현식을 평가한 값이 리턴된다.

이전까지 우리가 봐 온 것은 모두 부효과(side-effect)가 없는 순수한 리습 표현식이었다. 부효과란 표현식을 평가한 결과로 무언가의 상태가 변화되는 것을 말한다. 우리가 (+ 1 2)와 같은 순수한 리습 표현식을 평가했을 때는 아무런 부효과도 발생하지 않는다; 그저 값을 리턴할 뿐이다. 하지만 format을 호출하면, 값을 리턴하는 것과 동시에, 무언가가 출력된다. 이것은 부효과라고 할 수 있다.

우리가 부효과가 없는 코드를 작성한다고 하면, 함수의 몸체가 하나보다 많은 표현식으로 구성되는 일이 없을 것이다. 왜냐하면, 함수에서 리턴되는 값은 마지막 표현식의 값 뿐이고, 나머지 표현식들의 값은 버려지는데, 부효과가 전혀 없는 함수를 작성한다면, 부효과도 없고 리턴되지도 않을 표현식들을 함수에 넣어서 평가할 이유가 없기 때문이다.


2.10 변수

커먼 리습에서 가장 많이 쓰이는 오퍼레이터 중 하나는 새로운 지역 변수를 생성하는 let 이다:

> (let ((x 1) (y 2))
(+ x y))
3

let 표현은 두 부분으로 이루어져 있다. 첫 번째는 변수를 생성하는 부분인데 각각이 (변수, 표현식) 형태로 오게 되며, 각각의 변수는 표현식으로 초기화된다. 위의 예에서는 변수 x, y가 각각 1과 2로 초기화되었다. 이 변수들은 let의 몸체 안에서만 유효하다.

변수와 값의 목록 다음에는 몸체 부분이 나오고, 차례대로 평가된다. 몸체 부분의 표현식이 (+ x y) 하나 밖에 없다. 마지막 표현식의 값이 let 전체의 최종 값으로 리턴된다. let을 사용해서 askem을 조금 고쳐보자:

(defun ask-number ()
(format t "Please enter a number. ")
(let ((val (read)))
(if (numberp val)
val
(ask-number))))

이 함수는 val이라는 변수를 만들어서 read가 리턴하는 객체를 저장한다. val이 이 객체를 가지고 있기 때문에 함수가 값을 리턴하기 전에 그 값이 무엇인지 살펴보는 것이 가능하다. p로 끝나는 이름에서 알 수 있듯이 numberp는 인자가 숫자인지를 판단하는 predicate이다.

사용자가 입력한 값이 숫자가 아니라면, ask-numer는 자기 자신을 다시 호출한다. 결과적으로 함수는 숫자값을 얻을 때까지 계속해서 입력을 요구한다:

> (ask-number)
Please enter a number. a
Please enter a number. (ho hum)
Please enter a number. 52
52

여태가지 우리가 본 변수를 지역 변수라고 한다. 지역 변수는 특정한 문맥 하에서만 유효하다. 다른 형태의 변수도 있는데, 그것은 어디에서나 참조할 수 있는 전역 변수이다.

defparameter에 심볼과 값을 넘겨서 전역 변수를 만들 수 있다:

> (defparameter *glob* 99)
*GLOB*

전역 변수는 똑같은 이름으로 지역 변수가 생성되어 있는 표현식 내부를 제외하고, 어디서든 참조될 수 있다. 우연히 지역 변수가 전역 변수와 같은 이름을 가져, 전역 변수의 참조를 무효화시키는 일이 일어나는 것을 막기 위해서, 일반적으로 전역 변수의 이름은 별표(*)로 시작하고 끝난다. 우리가 방금 생성한 변수의 이름은 “star-glob-star”라고 읽는다.

defconstant를 사용하면 전역 상수 역시 정의할 수 있다:

(defconstant limit (+ *glob* 1))

상수의 이름을 특별히 구분해서 만들 필요는 없는데, 정의된 상수와 똑같은 이름의 변수를 사용하려고 하면 에러가 생기기 때문이다. 어떤 심볼이 전역 변수나 전역 상수의 이름으로 사용되고 있는지를 시험해 보려면 bounp 를 사용하면 된다:

> (boundp '*glob*)
T


2.11 대입

커먼 리습의 대표적인 대입 오퍼레이터는 setf 이다. setf를 사용해서 변수에 값을 대입할 수 있다:

> (setf *glob* 98)
98
> (let ((n 10))
(setf n 2)
n)
2

setf의 첫 번째 인자가 지역 변수의 이름을 나타내는 심볼이 아니라면, 그것은 전역 변수로 취급된다:

> (setf x (list 'a 'b 'c))
(A B C)

즉, 명시적으로 전역 변수를 선언하지 않고, 대입만으로도 전역 변수를 생성할 수 있다는 것이다. 하지만 defparameter를 사용해서 명시적으로 선언해 주는 것이 더 나은 방식이기 하다.

단순히 변수에 값을 대입하는 것 이상도 할 수 있다. setf의 첫 번째 인자는 변수의 이름뿐 아니라 표현식도 될 수 있다. 그런 경우에는 두 번째 인자의 값이 첫 번째 인자가 가리키는 장소로 들어간다.

> (setf (car x) 'n)
N
> x
(N B C)

setf의 첫 번째 인자는 특정한 장소를 가리키는 것이면 거의 무엇이든 될 수 있다. setf와 유사한 성격을 가진 오퍼레이터들을 “settable”하다고 하는데 이런 오퍼레이터들은 부록 D에 나와 있다.

setf에 넘길 수 있는 인자의 개수에는 제한이 없다. 다음 표현식은

(setf a b
c d
e f)

세 개의 setf를 따로 쓴 다음과 동일하다:

(setf a b)
(setf c d)
(setf e f)


2.12 Functional Programming

functional programming 이란 상태를 변화시키기 보다는 값을 리턴하는 식으로 프로그램을 작성하는 것을 말한다. 이것은 리습의 중요한 패러다임이다. 리습에 구현되어 있는 대부분의 함수들은 부효과를 만들어 내는 것이 아니라 값을 리턴한다.

예를 들어, remove 함수는 리스트와 객체를 받아서 그 객체를 제외한 리스트를 리턴한다:

> (setf lst '(c a r a t))
(C A R A T)
> (remove 'a lst)
(C R T)

remove를 설명할 때 리스트에서 객체를 제거한다고 하지 않고, 객체를 제외한 리스트를 리턴한다고 설명한 이유가 무엇일까? 실제로 그렇기 때문이다. 원래의 리스트는 전혀 변화가 없다:

> lst
(C A R A T)

만일 정말로 리스트에서 해당 객체를 제거하고 싶다면 어떻게 해야 할까? 리턴된 값을 setf를 사용해서 저장하면 그렇게 할 수 있다. 리스트 x에서 모든 a를 제거하고 싶다면 다음과 같이 하면 된다:

(setf x (remove 'a x))

functional programming이란 setf와 같은 것들을 사용하는 것을 되도록이면 피하는 것이다. 처음에는 ‘그런 식으로 프로그래밍을 하는 것이 과연 가능한가’ 라는 생각이 들 수도 있다. 어떻게 값을 리턴하는 것만으로 이루어진 프로그램을 만들 수 있을까?

부효과를 전혀 이용할 수 없다면 꽤나 불편할 것이다. 하지만 이 책을 계속해서 읽어나간다면, 부효과가 정말로 필요한 순간이 얼마나 드문가를 깨닫고 놀라게 될지도 모른다. 그리고 부효과를 덜 사용할수록, 프로그램은 더 나아진다.

functional programming의 가장 큰 장점 중 하나는 인터랙티브 테스팅을 가능하게 한다는 것이다. 순수하게 functional한 코드라면, 각각의 함수를 작성하자마자 따로따로 테스트하는 것이 가능하다. 테스트에서 함수가 기대했던 결과값을 리턴한다면, 그 함수가 올바로 작성됐다고 자신할 수 있다. 이런 자신감이 쌓이고 쌓이면 큰 차이를 만들어 낸다. 프로그램의 어디를 변경하던지 즉각적인 피드백을 받을 수 있다. 그리고 이런 즉각적인 피드백은, 마치 전화가 편지에 비해 즉각적인 메시지 전달이 가능하기 때문에 완전히 새로운 의사 소통 스타일을 만들어 내듯이, 새로운 스타일의 프로그래밍을 가능하게 한다.


2.13 반복 (Iteration)

무엇인가를 되풀이해서 하길 원할 때, 때때로 재귀보다는 반복을 이용하는 것이 나을 때가 있다. 반복을 이용해서 표를 만드는 예를 보자. 이 함수는

(defun show-squares (start end)
(do (( i start (+ i 1)))
((> i end) 'done)
(format t "~A ~A ~%" i (* i i))))

start에서부터 end까지의 수와 그 제곱을 출력한다:

> (show-squares 2 5)
2 4
3 9
4 16
5 25
DONE

do 매크로는 커먼 리습의 가장 기본적인 반복 오퍼레이터이다. let과 마찬가지로, do 역시 변수들을 만들어내는데, 첫 번째 인자로 변수를 선언하는 리스트가 온다. 이 리스트는 다음과 같이 이루어진다.

(변수 초기값 갱신값)
(variable initial update)

변수 자리에는 심볼이, 초기값과 갱신값 자리에는 표현식이 온다. 처음에는 변수가 초기값을 가진다. 매 반복마다 변수는 갱신값에 따라 값이 변한다. show-squares에서 do 문은 i 변수 하나만을 사용하는데, 시작할 때 i는 start 값이 되고, 계속적인 반복마다 1씩 증가한다.

do의 두 번째 인자는 하나 이상의 표현식을 가진 리스트가 되어야 한다. 첫 번째 표현식은 반복을 멈출 것인지를 여부를 결정하는 테스트가 된다. 이 경우에는 (i > end)가 만족되면 반복을 멈추게 된다. 나머지 표현식은 반복이 멈출 경우에 평가되고 그 결과가 do 전체의 값으로 리턴된다. 따라서 show-squares는 언제나 done을 리턴한다.

나머지 인자들은 루프의 몸체 부분을 구성하며 매 반복마다 순서대로 평가된다. 정리하면, 매 반복마다 변수가 갱신되고, 종료 조건이 테스트 된 후에, (테스트가 실패했다면) 몸체 부분이 평가된다.

show-squares의 재귀 버전을 비교해 보자:

(defun show-squares (i end)
(if (> i end)
'done
(progn
(format t “~A ~A~%” i (* i i))
(show-squares (+ i 1) end))))

새로운 원소는 progn 뿐이다. progn은 표현식들을 받아서 차례대로 평가한 후, 마지막 값을 리턴한다.

커먼 리습에는 덜 일반적인 경우를 위한 보다 단순한 반복 오퍼레이터도 있다. 리스트의 원소들을 차례대로 방문하기 위해서는 dolist 같은 것을 사용할 수 있다. 다음은 리스트의 길이를 리턴하는 함수이다:

(defun our-length (lst)
(let ((len 0))
(dolist (obj lst)
(setf len (+ len 1)))
len))

dolist는 (변수, 표현식)과 같은 형태의 인자와 함께, 몸체 부분을 받는다. 표현식이 리턴하는 리스트의 원소가 차례대로 변수의 값이 되고 그때마다 매번 몸체 부분이 평가된다. 따라서 위의 루프는 lst에 있는 매 obj마다 len을 증가시키라는 내용이 된다.

이 함수의 재귀적 버전을 다음처럼 작성할 수 있을 것이다:

(defun our-length (lst)
(if (null lst)
0
(+ (our-length (cdr lst)) 1)))

리스트가 비어있다면, 길이는 0이 된다. 그렇지 않다면, 리스트의 길이는 cdr의 길이에 1을 더한 것이다. 이 함수는 보기에 명확하지만, tail-recursive (섹션 13.2)하지는 않기 때문에, 속도 면에서 효율적이지는 않다.


2.14 함수는 객체이다

리습에서 함수는 심볼이나, 문자열, 리스트가 그렇듯이 일반적인 객체일 뿐이다. 우리가 function 에 어떤 함수의 이름을 주면, function은 그 함수에 해당하는 객체를 리턴할 것이다. quote처럼, function 역시 특수 오퍼레이터이다. 따라서 function의 인자에 quote를 붙일 필요는 없다:

> (function +)
#

이 이상해 보이는 리턴값은 일반적인 커먼 리습 구현에서 함수 객체를 표시하는 방식이다.

여태까지는 우리가 타이핑한 글자와 리습이 표시하는 값이 같은 객체들만 보아왔다. 함수는 다르다. 내부적으로, + 같은 함수는 기계어 코드의 세그먼트이다. 커먼 리습 구현에 따라 이와 같은 함수의 외부적 표현을 어떤 식으로 할 것인지가 달라질 수 있다.

우리가 quote의 약어로서 '를 사용하였듯이 function의 약어로 #'를 사용할 수 있다:

> #'+
#

이 약어는 sharp-quote 라고 읽는다.

다른 객체들과 마찬가지로, 함수 역시 인자로 넘길 수 있다. 함수를 인자로 받는 함수로 apply 가 있다. apply는 함수와 인자의 리스트를 받아서 인자들에 함수를 적용한 결과를 리턴한다:

> (apply #'+ '(1 2 3))
6
> (+ 1 2 3)
6

apply는 몇 개의 인자든 받을 수 있다. 마지막 인자가 리스트이기만 하면 된다:

> (apply #'+ 1 2 '(3 4 5))
15

funcall 함수는 apply와 똑같은 일을 하지만 인자들이 리스트로 싸여져 있지 않아도 된다는 점에서 다르다.

> (funcall #'+ 1 2 3)
6

사실 defun 매크로가 하는 일은 함수를 생성하고 거기에 이름을 붙이는 것이다. 하지만 이름이 없는 함수를 만들 때는 defun이 필요없다. 다른 리습 객체들과 마찬가지로 우리는 함수를 문자 그대로 참조할 수 있다.

정수를 문자 그대로 참조하기 위해서는, 숫자의 연속을 사용한다; 함수를 문자 그대로 참조하기 위해서는, 람다 표현식(lambda expression)이라 불리는 것을 사용할 수 있다. 람다 표현식은 ‘lambda 심볼, 매개변수의 리스트, 하나 이상의 표현식으로 이루어진 함수의 몸체’로 이루어져 있는 리스트이다.

두 개의 숫자를 받아서 그 합을 리턴하는 함수를 람다 표현식으로 표현해 보면 다음과 같다:

(lambda (x y)
(+ x y))

(x y)는 매개변수의 리스트이고 함수의 몸체가 그 다음에 따라온다.

람다 표현식 자체를 함수의 이름으로 생각할 수 있다. 일반적인 함수의 이름과 마찬가지로, 람다 표현식은 함수 호출에서 첫 번째 원소가 될 수 있다,

> ((lambda (x) (+ x 100)) 1)
101

그리고 #'를 람다 표현식에 붙임으로써, 그 함수를 얻어낼 수 있다,

> (funcall #'(lambda (x) (+ x 100))
1)
101

이 같은 표기 방식은 이름 없는 함수를 사용할 수 있게 해 준다.


Lambda가 무엇인가요?

람다 표현식의 lambda는 오퍼레이터가 아니다. 그것은 그저 심볼일 뿐이다. 리습의 초기 구현들에서는 lambda가 하는 역할이 있었다: 함수는 내부적으로 리스트로 표현되었고, 어떤 것이 그저 리스트인지 함수인지 구별하는 방법은 첫 번째 원소가 lambda 심볼인지를 보는 것이었다.

커먼 리습에서, 함수는 리스트로 표현되지만, 내부적으로는 리스트와 구별되는 함수 객체로서 표현된다. 따라서 lambda는 더 이상 실제적으로 필요하지 않다. 다음과 같이 표현하는 대신에

(lambda (x) (+ x 100))

다음과 같이 표현할 수도 있을 것이다.

((x) (+ x 100))

하지만 리습 프로그래머들이 함수의 시작을 lambda 심볼로 해왔기 때문에 커먼 리습에서는 그 같은 전통을 고수하고 있다.


2.15 타입

리습은 타입에 대해 유연한 접근방식을 가진다. 많은 언어에서, 변수들은 타입을 가져야 하고, 타입을 명세하지 않고는 변수를 사용할 수 없다. 커먼 리습에서는 변수가 아니라 값이 타입을 가진다. 모든 객체에 그 객체가 어떤 타입인지 나타내는 이름표가 붙어 있다고 생각해 보자. 이런 방식을 manifest typing이라고 한다. 커먼 리습에서는 변수의 타입을 선언할 필요가 없다. 왜냐하면 변수가 어떤 타입의 객체든 참조할 수 있기 때문이다.

타입의 선언이 반드시 필요하지는 않음에도 불구하고, 실행 속도를 빠르게 하기 위해서 타입을 명시적으로 선언할 수 있다. 타입 선언에 대해서는 섹션 13.3에서 다룬다.

내장되어 있는 커먼 리습의 타입들은 하위 타입과 상위 타입의 계층 구조를 이룬다. 모든 객체는 하나 이상의 타입을 가지고 있다. 예를 들어, 숫자 27은 fixnum, integer, rational, real, number, atom, t의 타입(뒤로 갈수록 일반적인 타입이다.)에 속한다. (수 타입에 대해서는 9장에서 설명한다.) 타입 t는 모든 타입의 상위 타입이다. 따라서 모든 것은 타입 t에 속한다.

typep 함수는 객체와 타입 지정자를 받아서 그 객체가 해당 타입이면 참을 리턴한다:

> (typep 27 'integer)
T

이후에 여러 가지 내장형 타입들에 대해서 살펴보게 될 것이다.


2.16 계속해서 나아가기

이 장에서 우리는 리습을 겉핥기 식으로 살펴봤을 뿐이다. 매우 이상한 언어라는 느낌을 받았는지도 모르겠다. 처음에는, 리습이 단일한 프로그램 형태를 가지고 있다는 것을 설명했다. 이 형태는 리습의 객체 중에 하나인 리스트에 기반을 두고 있다. 리습 자체도 사용자가 정의할 수 있는 함수와 아무 차이가 없는 리습 함수로 이루어진 리습 프로그램이다.

이 같은 설명이 명확하게 이해되지 않는다고 해서 걱정할 필요는 없다. 리습은 그것들에 익숙해지고 사용하기까지 다소 시간이 걸릴 수 있는 고급 개념들을 많이 가지고 있다. 적어도 하나는 확실하다: 리습에는 무언가 놀랄만한 굉장한 것이 있다.

리처드 가브리엘이 반 농담조로 C를 유닉스를 작성하기 위한 언어로 묘사한 적이 있다. 우리도 그와 비슷하게 리습을 리습을 작성하기 위한 언어로 정의할 수 있다. 하지만 두 문장 사이에는 차이점이 있다. 자기 스스로를 작성하기 위한 언어는 무언가 특정한 종류의 어플리케이션을 작성하기 위한 언어와 근본적으로 다르다. 여기에 새로운 프로그래밍의 길이 있다: 언어를 사용해서 프로그램을 작성하는 것과 동시에, 언어 자체를 개선시켜서 당신이 원하는 프로그램에 적합하게 만들어 나갈 수 있다. 프로그램을 언어가 표현할 수 있는 데까지 끌어 내리는 것이 아니라, 언어를 쌓아 올려서 만들고자 하는 프로그램을 간단하게 표현할 수 있게 하는 것, 그것이 리습 프로그래밍의 정수이다.


translated by 찬우

Emacs에서 Rails 코딩하기

환경: 맥북, 레오파드

레오파드에는 Emacs가 /usr/share/emacs에 기본적으로 깔려있다.

루비와 레일즈 관련 파일들을 위치시켜야 하는 emacs 폴더:
~/emacs.d/ 또는 /usr/share/emacs/site-lisp
둘 중 어느 곳에 위치시키든, 또는 전혀 다른 곳에 위치시키든 상관 없는 것 같다. 뒤에 작성할 .emacs 파일에 파일들이 위치한 경로를 적어주면 된다.

위에서 정한 경로 내에 넣어야 하는 파일:
1. emacs-rails 파일들
http://rubyforge.org/projects/emacs-rails
다음 명령으로도 다운받을 수 있다.
svn co svn://rubyforge.org/var/svn/emacs-rails/trunk emacs-rails

2. snippet.el 파일
http://www.kazmier.com/computer/snippet.el
find-recursive.el 파일
http://www.webweavertech.com/ovidiu/emacs/find-recursive.txt 에서 find-recursive.txt 파일을 받아서 확장자를 el로 변경
inf-ruby.el 파일
http://svn.ruby-lang.org/cgi-bin/viewvc.cgi/trunk/misc/inf-ruby.el?view=co
그런데 이 파일은 '3. ruby 관련 파일'에서 다운 받으면 그 안에 들어있기 때문에 여기서 다운받을 필요는 없을 것 같다.

3. ruby 관련 파일
위의 파일들만 받으면 ruby-mode와 관련해서 에러가 나기 때문에 ruby 관련 el 파일들도 받아야 하는 것 같다.
http://svn.ruby-lang.org/repos/ruby/trunk/misc/ 이 곳에서 다운 받거나 다음 명령으로도 다운받을 수 있다.
svn export http://svn.ruby-lang.org/repos/ruby/trunk/misc ruby

위와 같은 파일들을 다운 받았으면 rails를 로드하는 .emacs 파일을 작성하여 홈 폴더(~)에 저장해야 한다. site-lisp 디렉토리나 루트(/)에 놓아서는 emacs에 의해 로드되지 않는 것 같았다. .emacs파일의 내용은 다음과 같다.

(setq load-path (cons "파일들이 위치한 경로명" load-path))
(require 'rails)

또는 다음과 같이 작성해도 된다.

(add-to-list 'load-path "파일들이 위치한 경로명")
(require 'rails)

하지만 계속해서 나는 다음과 같은 에러.
Symbol's value as variable is void: dirname
온갖 방법을 다 동원해 봤지만 원인을 알 수가 없었다. 에러에도 불구하고 emacs에서 Meta+X - ruby-mode를 쳐서 ruby-mode로의 전환은 가능했다. 하지만 emacs 시작시에 나는 저 에러는 그냥 넘어가기엔 너무 찜찜하다.

계속해서 실패하다가, 드디어 원인을 알아냈다. snippet.el 파일과 find-recursive.el 파일을 해당 링크로 가서 내용을 복사한 다음 emacs에 붙여넣어 두 파일을 만들었었는데, 붙여넣는 과정에서 잘못이 생긴 듯 싶다. 두 파일을 다시 '링크를 다른 이름으로 저장'하는 식으로 다운받아서 해 보니 모든 것이 순조롭게 돌아간다.

그리고 http://sodonnell.wordpress.com/the-emacs-newbie-guide-for-rails/ 가 가장 설명이 잘 되어 있는 것 같다. 다운 받아야 하는 각 부분이 어떤 역할을 하는지를 잘 설명해 주고 있다. 이 사이트에서는 ruby-mode와 ruby-electric, snippet, find-recursive를 명시적으로 require 하는 식으로 .emacs 파일을 작성했다. 하지만 다른 사이트에서는 .emacs의 내용이 로드할 디렉토리를 지정한 다음 (require 'rails)만 적어주면 된다고 하는 것 같다. 후자처럼 해도 def를 치면 end가 자동으로 생기는 것과 같은 ruby-electric의 기능도 제대로 동작하는 것 같고.. 차이를 잘 모르겠다.

그리고 이 사이트에서는 ECB(emacs code browser)에 대해서도 얘기하고 있는데, ECB를 사용하면 Emacs에서 파일을 열 때 폴더구조와 파일명이 보이기 때문에 편하다. 그런데 이 사이트가 ECB 명령어를 좀 이상하게 적어놓고 있어서 한참 삽질을 하게 만든다; 삽질끝에 알아낸 명령어는 다음과 같다.

ECB 활성화: meta+x ecb-activate
ECB 비활성화: meta+x ecb-deactivate
Jump to the directory window: ctrl+c .gd
Jump to the history window: ctrl+c .gh
Jump to the last window you were in: ctrl+c .gl
Jump to the first editor window ctrl+c .g1

그리고 테스트로 rhtml 파일을 생성해서 편집해 보았는데 html 태그의 indentation이 전혀 제대로 되지 않는다. 어쩔 수 없이 emacs에 원래 있는 html-mode로 전환해서 편집해야 한다.

핫 그런데 방금 rails 프로젝트의 view 디렉토리 내에서 rthml 파일을 생성하니 HTML view RoR이라고 모드가 제대로 뜬다.


References

Emacs for Rails << Software bits and pieces
http://sodonnell.wordpress.com/the-emacs-newbie-guide-for-rails/
rubyforge
http://rubyforge.org/projects/emacs-rails
rails on emacs
http://dima-exe.ru/rails-on-emacs
ruby: installing emacs extensions
http://www.rubygarden.org/Ruby/page/show/InstallingEmacsExtensions
How to use emacs with rails
http://wiki.rubyonrails.org/rails/pages/HowToUseEmacsWithRails
emacs에서 rails를 다루자
http://cafe.naver.com/amagramer.cafe?iframe_url=/ArticleRead.nhn%3Farticleid=488


나의 .emacs 코드

;;Rails with Emacs

;Allows syntax highlighting to work, among other things
(global-font-lock-mode 1)

; loads ruby mode when a .rb file is opened.
(add-to-list 'load-path "/Users/chanwoo/.emacs.d/ruby")
(autoload 'ruby-mode "ruby-mode" "Major mode for editing ruby scripts." t)
(setq auto-mode-alist (cons '(".rb$" . ruby-mode) auto-mode-alist))
(setq auto-mode-alist (cons '(".rhtml$" . html-mode) auto-mode-alist))

; adding ruby electric
(add-hook 'ruby-mode-hook
(lambda()
(add-hook 'local-write-file-hooks
'(lambda()
(save-excursion
(untabify (point-min) (point-max))
(delete-trailing-whitespace)
)))
(set (make-local-variable 'indent-tabs-mode) 'nil)
(set (make-local-variable 'tab-width) 2)
(imenu-add-to-menubar "IMENU")
(define-key ruby-mode-map "\C-m" 'newline-and-indent) ;Not sure if this line is 100% right but it works!
(require 'ruby-electric)
(ruby-electric-mode t)
))

;These lines are required for ECB
(add-to-list 'load-path "/Users/chanwoo/.emacs.d/eieio-0.17")
(add-to-list 'load-path "/Users/chanwoo/.emacs.d/speedbar-0.14beta4")
(add-to-list 'load-path "/Users/chanwoo/.emacs.d/semantic-1.4.4")
(setq semantic-load-turn-everything-on t)
(require 'semantic-load)

; This installs ecb - it is activated with M-x ecb-activate
(add-to-list 'load-path "/Users/chanwoo/.emacs.d/ecb-snap")
(require 'ecb-autoloads)

(setq ecb-gzip-setup (quote cons))
(setq ecb-layout-name "left14")
(setq ecb-layout-window-sizes (quote (("left14" (0.2564102564102564 . 0.6949152542372882) (0.2564102564102564 . 0.23728813559322035)))))
(setq ecb-source-path (quote ("~/work")))

; needed for rails mode
(add-to-list 'load-path "/Users/chanwoo/.emacs.d/snippet")
(require 'snippet)
(require 'find-recursive)

(add-to-list 'load-path "/Users/chanwoo/.emacs.d/emacs-rails")
(require 'rails)

Installing Common Lisp - SBCL

환경: 맥북, 레오파드

레오파드에는 Emacs가 /usr/share/emacs에 기본적으로 깔려 있다. 리습 구현(implementation)과 SLIME(The Superior Lisp Interaction Mode for Emacs, 루비의 irb 같은 것이다. 물론 이런 방식의 프로그래밍 환경을 제공한 것은 리습이 제일 처음인 것으로 알고 있다.)만 설치하고 Emacs와 연결시키면 된다.

리습은 다른 언어와 달리 구현이 여러 개다. 각 구현마다 지원하는 바가 조금씩 다르고.. 따라서 자신의 취향에 맞는 구현을 선택하면 된다. 구현에는 Clisp, Allegro Common Lisp, SBCL, OpenMCL, Lispworks 등등이 있는데 Lispworks같은 경우에는 IDE 형태로 지원이 되고 깔끔하지만 상용이다. 상용 리습 구현들은 그 값을 하기 위해서 리습으로 짠 코드를 데스크탑 어플리케이션으로 딜리버리 하는 것을 쉽게 해 준다거나, 기술적 지원들을 해준다고 하지만, 당장 나와는 관계가 없다. 개인 버전은 무료라지만 여러 가지 제약이 많기 때문에 그냥 Emacs에 다른 리습 구현을 연결해 사용하는 게 나을 것 같다. Clisp을 받으려고 했다가 맥용이 어떤 것인지가 확실치 않아서, 맥 플랫폼 버전이 명시되어 있는 SBCL을 다운받았다.

다운받고 압축을 푼 후 해당 디렉토리로 가서
sudo sh install.sh
을 쳤는데, 'GNU Make not found'라고 나오면서 설치가 안된다. 패스의 문제인가 하고 set을 쳐 봤는데 PATH에 /usr/bin은 추가가 되어 있다. /usr/bin을 살펴보니 make가 있어야 하는데 make가 없다. Xcode 설치를 안 하면 이렇다.

Xcode를 설치하려면 레오파드 설치 시디를 넣은 후 Optional Installs를 선택하고 Xcode Tools를 선택해서 XcodeTools.mpkg 를 더블클릭하면 된다. 그러면 make나 gcc등이 다 설치가 된다.

다시 SBCL 압축을 푼 폴더로 이동해서 sudo sh install.sh를 치니 /usr/local/bin에 정상적으로 설치가 된다. 이제 SLIME을 다운받자. SLIME은 다운받고 압축을 풀기만 하면 되고 별도의 설치가 필요없다. 내 경우는 /usr/local/bin/slime-2.1로 복사해 놓았다. (다운받은 디렉토리에서 'sudo cp -r slime-2.1 /usr/local/bin' 과 같이 하면 /usr/local/bin으로 복사된다. 복사 후 원본 디렉토리를 지우려면 'rm -rf 디렉토리명'을 치면 된다.)

이제 .emacs 파일에 구현과 SLIME을 부르는 부분만 추가하면 된다. .emacs 파일은 홀 폴더(~) 안에 있다.('ls -a'로 검색하면 .로 시작하는 파일도 다 보인다.) 없다면 새로 만들면 된다.('emacs .emacs') .emacs 파일 안에 다음 내용을 추가한다.(다운받은 SLIME 폴더의 README 파일에 어떻게 해야 되는지 나와 있다.)

(add-to-list 'load-path "/usr/local/bin/slime-2.1") ;your SLIME directory
(setq inferior-lisp-program "/usr/local/bin/sbcl") ;your Lisp system
(require 'slime)
(slime-setup)

물론 ';' 뒤의 주석은 칠 필요 없고, 디렉토리 명은 자신의 SLIME과 리습 구현이 있는 디렉토리를 적어야 한다.

Emacs 를 시작한 뒤, Meta+x slime 을 치면 SLIME이 시작되면서 우아하게 CL-USER>가 등장한다. 만일 터미널에서 Emacs를 실행했을 때 메타 키가 먹지 않는다면 터미널 -> 환경 설정 -> 키보드 -> 'option을 메타 키로 사용'에 체크한다.