EVAL-WHEN 특수 연산자
EVAL-WHEN 특수 연산자 (eval-when)
eval-when은 Common Lisp의 특수 연산자로, 몸체 폼을 어떤 상황(situation)에서 평가할지 지정해요. 컴파일 타임, 로드 타임, 실행 타임 가운데 상황을 골라서 해당 시점에만 몸체가 실행되게 할 수 있어요. 매크로를 쓸 때 컴파일러의 환경을 준비하거나, 같은 정의를 여러 타이밍에 걸쳐 실행하고 싶을 때 특히 유용해요.
문법 (Syntax)
eval-when (situation*) form* => result*
인자와 값 (Arguments and Values)
situation—:compile-toplevel,:load-toplevel,:execute기호들 중 하나이거나,compile,load,eval이에요. (eval,compile,load사용은 더 이상 쓰지 말라고 권장돼요.)forms— 암시적 progn이에요.results— 몸체가 실행되면form들의 값이고, 실행되지 않으면nil이에요.
설명 (Description)
eval-when 폼의 몸체는 암시적 progn으로 처리되지만, 나열된 상황에서만 처리돼요.
:compile-toplevel(또는 compile)과 :load-toplevel(또는 load) 상황의 사용은, compile-file이 처리하는 코드에서 eval-when이 **최상위 폼(top level form)**으로 나타날 때 평가가 일어나는지와 시점을 제어해요. Section 3.2.3 (File Compilation)을 보세요.
:execute(또는 eval) 상황의 사용은 다른 eval-when 폼, 즉 최상위 폼이 아니거나 eval·compile이 처리한 코드에서 평가가 일어날지를 제어해요. 그런 폼에서 :execute 상황이 지정되면 몸체 폼을 암시적 progn으로 처리하고, 그렇지 않으면 eval-when 폼은 nil을 반환해요.
eval-when은 보통 최상위 폼으로 나타나지만, 최상위가 아닌 폼으로 나타나는 것도 의미가 있어요. 다만 Section 3.2 (Compilation)에서 설명하는 컴파일 타임 부수 효과는 eval-when이 최상위 폼으로 나타날 때만 일어나요.
예제 (Examples)
eval-when의 대표적 용법 하나는, 사용자 정의 리더 매크로를 쓸 때 컴파일러가 파일을 제대로 읽게 만드는 거예요.
(eval-when (:compile-toplevel :load-toplevel :execute)
(set-macro-character #\$ #'(lambda (stream char)
(declare (ignore char))
(list 'dollar (read stream))))) => T
이것은 set-macro-character 호출이 컴파일러의 실행 환경에서 실행되게 해서, 컴파일러의 리더 구문 테이블을 수정해요.
아래 예시들은 최상위/비최상위 위치에 따라 평가가 어떻게 달라지는지 보여줘요.
;;; The EVAL-WHEN in this case is not at toplevel, so only the :EXECUTE
;;; keyword is considered. At compile time, this has no effect.
;;; At load time (if the LET is at toplevel), or at execution time
;;; (if the LET is embedded in some other form which does not execute
;;; until later) this sets (SYMBOL-FUNCTION 'FOO1) to a function which
;;; returns 1.
(let ((x 1))
(eval-when (:execute :load-toplevel :compile-toplevel)
(setf (symbol-function 'foo1) #'(lambda () x))))
;;; If this expression occurs at the toplevel of a file to be compiled,
;;; it has BOTH a compile time AND a load-time effect of setting
;;; (SYMBOL-FUNCTION 'FOO2) to a function which returns 2.
(eval-when (:execute :load-toplevel :compile-toplevel)
(let ((x 2))
(eval-when (:execute :load-toplevel :compile-toplevel)
(setf (symbol-function 'foo2) #'(lambda () x)))))
;;; If this expression occurs at the toplevel of a file to be compiled,
;;; it has BOTH a compile time AND a load-time effect of setting the
;;; function cell of FOO3 to a function which returns 3.
(eval-when (:execute :load-toplevel :compile-toplevel)
(setf (symbol-function 'foo3) #'(lambda () 3)))
;;; #4: This always does nothing. It simply returns NIL.
(eval-when (:compile-toplevel)
(eval-when (:compile-toplevel)
(print 'foo4)))
;;; If this form occurs at toplevel of a file to be compiled, FOO5 is
;;; printed at compile time. If this form occurs in a non-top-level
;;; position, nothing is printed at compile time. Regardless of context,
;;; nothing is ever printed at load time or execution time.
(eval-when (:compile-toplevel)
(eval-when (:execute)
(print 'foo5)))
;;; If this form occurs at toplevel of a file to be compiled, FOO6 is
;;; printed at compile time. If this form occurs in a non-top-level
;;; position, nothing is printed at compile time. Regardless of context,
;;; nothing is ever printed at load time or execution time.
(eval-when (:execute :load-toplevel)
(eval-when (:compile-toplevel)
(print 'foo6)))
영향 (Affected By)
없음.
예외 상황 (Exceptional Situations)
없음.
함께 보기 (See Also)
compile-file- Section 3.2 (Compilation)
참고 (Notes)
eval-when의 정의에서 따라나오는 논리적 결과가 몇 가지 있어요.
- 단일
eval-when표현식을 실행하면 몸체 코드는 기껏해야 한 번 실행돼요. - 최상위 폼에서 쓰일 매크로는, 부수 효과가 매크로 확장물 안의 폼들에 의해 일어나도록 작성돼야 해요. 매크로 확장자(expander) 자체가 부수 효과를 내면 안 돼요.
예를 들어 잘못된 방식과 올바른 방식을 비교해 볼게요.
잘못된 방식:
(defmacro foo ()
(really-foo)
`(really-foo))
올바른 방식:
(defmacro foo ()
`(eval-when (:compile-toplevel :execute :load-toplevel) (really-foo)))
이 관례를 지키면 그런 매크로가 최상위가 아닌 폼으로 나타나도 직관적으로 동작해요.
eval-when주변에 변수 바인딩을 두면 그 바인딩을 확실히 포착(capture)할 수 있어요. 컴파일 타임-동시 모드(compile-time-too)는 일어날 수 없으니까요. 즉 변수 바인딩을 도입하면eval-when은 최상위 폼이 아니게 되죠.
(let ((x 3))
(eval-when (:execute :load-toplevel :compile-toplevel) (print x)))
이것은 실행(즉 로드) 시점에 3을 출력하고, 컴파일 시점에는 아무것도 출력하지 않아요. defun과 defmacro의 확장이 eval-when으로 표현되면서 어휘 환경을 올바르게 포착할 수 있게 해 주는 중요한 성질이에요.
(defun bar (x) (defun foo () (+ x 3)))
이것은 대략 이렇게 확장될 수 있어요.
(defun bar (x)
(progn (eval-when (:compile-toplevel)
(compiler::notice-function-definition 'foo '(x)))
(eval-when (:execute :load-toplevel)
(setf (symbol-function 'foo) #'(lambda () (+ x 3))))))
bar의 정의가 최상위 폼이 아닐 때, 이 코드는 위 규칙들에 따라 이것과 똑같이 취급돼요.
(defun bar (x)
(setf (symbol-function 'foo) #'(lambda () (+ x 3))))