CASE, CCASE, ECASE 매크로

CASE, CCASE, ECASE 매크로 (case, ccase, ecase)

case식별(identity) 기준으로 키를 맞춰 분기하는 조건 매크로예요. 한 값이 여러 키 중 하나와 같을 때(clause) 해당 본문을 실행합니다. 세 형제 case·ccase·ecase는 기본 동작은 같지만, 어떤 clause도 매칭되지 않았을 때의 처리가 서로 달라요. 값의 후보를 여러 개 붙여 나열할 수 있어 if·cond보다 읽기 쉬운 분기를 만들 때 씁니다.

출처: CLHS: Macro CASE, CCASE, ECASE

문법 (Syntax)

case keyform {normal-clause}* [otherwise-clause] => result*
ccase keyplace {normal-clause}* => result*
ecase keyform {normal-clause}* => result*

normal-clause::= (keys form*)

otherwise-clause::= ({otherwise | t} form*)

clause::= normal-clause | otherwise-clause

인자와 값 (Arguments and Values)

  • keyform — 폼이며, 평가되어 test-key를 만들어요.
  • keyplace — 폼이며, 처음에 평가되어 test-key를 만들고, 어떤 키도 매칭되지 않을 때 나중에 place로 쓰일 수 있어요.
  • test-keykeyform 또는 keyplace의 평가로 만들어지는 객체예요.
  • keys — 객체 리스트의 지정자(designator)예요. case의 경우 심볼 totherwise는 keys 지정자로 쓸 수 없어요. 이 심볼 자체를 키로 쓰려면 각각 (t), (otherwise) 지정자를 써야 해요.
  • forms — 암시적 progn(implicit progn)이에요.
  • results — 매칭된 clause 안의 forms가 돌려주는 값들이에요.

설명 (Description)

이 매크로들은 식별(identity) 기준으로 test-key를 매칭해 선택된 clause의 본문을 조건부로 실행하게 해줘요.

keyform 또는 keyplace를 평가해 test-key를 만들어요. 그 다음 각 normal-clause를 차례로 살펴봐요. test-key가 그 clause의 어떤 키와 같으면, 그 clause의 forms를 암시적 progn으로 평가하고, 그 값들이 case·ccase·ecase 폼의 값으로 반환돼요.

매칭되는 normal-clause가 없을 때의 동작에서만 셋이 갈라져요.

  • case — 매칭되는 normal-clause가 없는데 otherwise-clause가 있으면 그 clause가 자동으로 매칭돼요. otherwise-clause가 없으면 casenil을 돌려줘요.
  • ccase — 매칭되는 normal-clause가 없으면 type-error 타입의 정정 가능한 오류를 신호해요. 문제의 데이터는 test-key이고, 기대 타입은 (member key1 key2 ...)와 동치인 타입이에요. store-value restart로 오류를 고칠 수 있어요. store-value restart가 호출되면 그 인자가 새 test-key가 되어 (setf keyplace test-key)처럼 keyplace에 저장되고, ccase는 각 clause를 다시 살피며 처음부터 다시 시작해요. keyplace의 하위 폼은 어떤 case도 성립하지 않을 때 다시 평가될 수 있어요.
  • ecase — 매칭되는 normal-clause가 없으면 type-error 타입의 정정 불가능한 오류를 신호해요. ccase와 달리 ecasenormal-clause가 매칭되지 않으면 돌아오지 않는다는 사실에 호출자가 의존할 수 있어요.

예제 (Examples)

(dolist (k '(1 2 3 :four #\v () t 'other))
  (format t "~S "
     (case k ((1 2) 'clause1)
             (3 'clause2)
             (nil 'no-keys-so-never-seen)
             ((nil) 'nilslot)
             ((:four #\v) 'clause4)
             ((t) 'tslot)
             (otherwise 'others)))) 
>>  CLAUSE1 CLAUSE1 CLAUSE2 CLAUSE4 CLAUSE4 NILSLOT TSLOT OTHERS 
=>  NIL

이 예제가 case의 미묘한 점을 보여줘요. nil을 keys 지정자로 쓰면 (nil)이라는 키 목록과 구별돼요. 그래서 값 ()(빈 리스트, 즉 nil)는 no-keys-so-never-seen이 아니라 nilslot에 걸려요. 키 심볼 t(t)로 써야 다른 t clause와 정확히 매칭되는 걸 볼 수 있어요.

(defun add-em (x) (apply #'+ (mapcar #'decode x)))
=>  ADD-EM
(defun decode (x)
  (ccase x
    ((i uno) 1)
    ((ii dos) 2)
    ((iii tres) 3)
    ((iv cuatro) 4)))
=>  DECODE
(add-em '(uno iii)) =>  4
(add-em '(uno iiii))
>>  Error: The value of X, IIII, is not I, UNO, II, DOS, III,
>>         TRES, IV, or CUATRO.
>>   1: Supply a value to use instead.
>>   2: Return to Lisp Toplevel.
>>  Debug> :CONTINUE 1
>>  Value to evaluate and use for X: 'IV
=>  5

ccase 예제는 매칭이 안 되면 오류를 내고, 사용자가 store-value restart로 값 'IV를 제공해 5(= UNO 1 + IV 4)라는 결과를 얻는 흐름을 보여줘요.

부수 효과 (Side Effects)

디버거에 들어갈 수 있어요. store-value restart가 호출되면 keyplace의 값이 바뀔 수 있어요.

영향 (Affected By)

ccaseecase는 오류를 신호할 수 있으므로, 기존 핸들러와 *debug-io*의 영향을 받을 수 있어요.

예외 상황 (Exceptional Situations)

ccaseecase는 매칭되는 normal-clause가 없으면 type-error 타입 오류를 신호해요.

함께 보기 (See Also)

  • cond, typecase, setf, Section 5.1 (Generalized Reference)

참고 (Notes)

다음은 casecond 기반 확장을 보여주는 동치식이에요.

(case test-key
  {((key*) form*)}*)
== 
(let ((#1=#:g0001 test-key))
  (cond {((member #1# '(key*)) form*)}*))

ecaseccase가 쓰는 구체적인 오류 메시지는 구현마다 달라질 수 있어요. 메시지의 정확한 표현을 제어해야 하는 상황이라면, otherwise-clause를 두고 적절한 메시지로 오류를 신호하는 case를 쓰는 편이 나아요.

더 알아보기 (Learn more)