CASE, CCASE, ECASE 매크로
CASE, CCASE, ECASE 매크로 (case, ccase, ecase)
case는 식별(identity) 기준으로 키를 맞춰 분기하는 조건 매크로예요. 한 값이 여러 키 중 하나와 같을 때(clause) 해당 본문을 실행합니다. 세 형제 case·ccase·ecase는 기본 동작은 같지만, 어떤 clause도 매칭되지 않았을 때의 처리가 서로 달라요. 값의 후보를 여러 개 붙여 나열할 수 있어 if·cond보다 읽기 쉬운 분기를 만들 때 씁니다.
문법 (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-key—keyform또는keyplace의 평가로 만들어지는 객체예요.keys— 객체 리스트의 지정자(designator)예요.case의 경우 심볼 t와 otherwise는 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가 없으면case는nil을 돌려줘요. - ccase — 매칭되는
normal-clause가 없으면 type-error 타입의 정정 가능한 오류를 신호해요. 문제의 데이터는 test-key이고, 기대 타입은(member key1 key2 ...)와 동치인 타입이에요. store-value restart로 오류를 고칠 수 있어요.store-valuerestart가 호출되면 그 인자가 새 test-key가 되어(setf keyplace test-key)처럼keyplace에 저장되고,ccase는 각 clause를 다시 살피며 처음부터 다시 시작해요.keyplace의 하위 폼은 어떤 case도 성립하지 않을 때 다시 평가될 수 있어요. - ecase — 매칭되는
normal-clause가 없으면 type-error 타입의 정정 불가능한 오류를 신호해요.ccase와 달리ecase는normal-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)
ccase와 ecase는 오류를 신호할 수 있으므로, 기존 핸들러와 *debug-io*의 영향을 받을 수 있어요.
예외 상황 (Exceptional Situations)
ccase와 ecase는 매칭되는 normal-clause가 없으면 type-error 타입 오류를 신호해요.
함께 보기 (See Also)
cond,typecase,setf, Section 5.1 (Generalized Reference)
참고 (Notes)
다음은 case의 cond 기반 확장을 보여주는 동치식이에요.
(case test-key
{((key*) form*)}*)
==
(let ((#1=#:g0001 test-key))
(cond {((member #1# '(key*)) form*)}*))
ecase와 ccase가 쓰는 구체적인 오류 메시지는 구현마다 달라질 수 있어요. 메시지의 정확한 표현을 제어해야 하는 상황이라면, otherwise-clause를 두고 적절한 메시지로 오류를 신호하는 case를 쓰는 편이 나아요.