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 도 설치할 거냐고 물어보는데 역시 선택한다. 이 두 항목이 체크되면 메뉴의 '적용'을 누른다. 이 둘을 설치한 후 구글 툴바를 다시 설치하면 즐겨찾기가 제대로 보이는 것을 볼 수 있다.