EVAL-WHEN 특수 연산자

EVAL-WHEN 특수 연산자 (eval-when)

eval-when은 Common Lisp의 특수 연산자로, 몸체 폼을 어떤 상황(situation)에서 평가할지 지정해요. 컴파일 타임, 로드 타임, 실행 타임 가운데 상황을 골라서 해당 시점에만 몸체가 실행되게 할 수 있어요. 매크로를 쓸 때 컴파일러의 환경을 준비하거나, 같은 정의를 여러 타이밍에 걸쳐 실행하고 싶을 때 특히 유용해요.

출처: CLHS: Special Operator EVAL-WHEN

문법 (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을 출력하고, 컴파일 시점에는 아무것도 출력하지 않아요. defundefmacro의 확장이 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))))

더 알아보기 (Learn more)