condは任意の条件式を並べる汎用の分岐、caseはeqlによる値一致に特化した簡潔な分岐、ecaseはマッチしない値を即座にエラーにする網羅強制版という階層関係にあります。caseのキーにはリストで複数値をまとめて指定でき、otherwiseでデフォルト節を書けますが、文字列やコンスセルはeqlで比較できないため使えません。- 年月日から曜日を求める実装を通じて、うるう年判定に
cond、月の日数取得にcase、曜日番号とシンボルの対応にecaseを使い分ける具体例を示しています。
1. 問題:日付から曜日を求める
年月日から曜日名を返す問題を解いていく中で、cond・case・ecase の使い方を整理していきます。
完成形はこうです。
(day-symbol 2026 5 9)
;=> :SATURDAYCode language: Lisp (lisp)
1.1. condとcaseとecaseの関係
condは最も汎用で、任意の条件式を並べられます。- 条件が「1つの式とさまざまな値の一致」に限定されるなら
caseで簡潔に書けます。 - さらに、全ケースを網羅することが仕様上保証されるなら
ecaseにすると、想定外の値を即座にエラー検出できます。
| 条件式 | 比較 | マッチなし | |
|---|---|---|---|
cond | 任意の式 | 任意 | nil(tでデフォルト値を指定できる) |
case | eql 一致 | eql | nil(t/otherwise でデフォルト値を指定できる) |
ecase | eql 一致 | eql | type-error |
ccase | eql 一致 | eql | 修正可能エラー |
typecase | type 一致 | typep | nil(t/otherwise でデフォルト値を指定できる) |
(defun leap-year-p (y)
(cond
((zerop (mod y 400)) t)
((zerop (mod y 100)) nil)
((zerop (mod y 4)) t)
(t nil)))
(defun days-in-month (m y)
(case m
((4 6 9 11) 30)
(2 (if (leap-year-p y) 29 28))
(otherwise 31)))
(defun weekday-symbol (n)
(ecase n
(0 :monday)
(1 :tuesday)
(2 :wednesday)
(3 :thursday)
(4 :friday)
(5 :saturday)
(6 :sunday)))Code language: Lisp (lisp)
case と ecase の違いはマッチしなかったときの挙動だけです。
「マッチしない値が来ることがありうる」なら case の otherwise、「マッチしない値は仕様上ありえない」なら ecase という使い分けになります1。
比較関数は同じ eql で、キーに使える型も変わりません。
2. condで任意の条件を並べる
cond の基本形はこうです。
(cond (条件式1 値1)
(条件式2 値2)
(t デフォルト値))Code language: Lisp (lisp)
条件式は上から順に評価され、最初に真になった節の値を返します。t は常に真なので、最後に置けばデフォルト節として機能します。t を省いてどの節にもマッチしなかった場合は nil が返ります2。
まず、うるう年判定を書きます。
うるう年の条件は「400の倍数」「100の倍数でなく4の倍数」の2つで、それ以外は平年です。
(defun leap-year-p (y)
(cond
((zerop (mod y 400)) t)
((zerop (mod y 100)) nil)
((zerop (mod y 4)) t)
(t nil)))
(leap-year-p 2025) ;=> NIL
(leap-year-p 2000) ;=> T
(leap-year-p 1900) ;=> NILCode language: Lisp (lisp)
cond にすると同じ条件が横並びになり、判定の優先順位が読みやすくなります。
ただし、条件節の順序は重要です。
400の倍数を先に判定しないと、100の倍数の節に吸い取られてしまいます3。
2.1. cond は2分岐の if よりネストが深くならない
ちなみに、この判定を、2分岐の if で書くと、3段のネストが必要になります。
(defun leap-year-p/if (y)
(if (zerop (mod y 400))
t
(if (zerop (mod y 100))
nil
(if (zerop (mod y 4))
t
nil))))Code language: Lisp (lisp)
3. caseでeql一致の分岐を書く
case は、 cond のうち「1つの式をさまざまな値と比較する」パターンを簡潔に書く構文です。
(case キー式
(値1 結果1)
(値2 結果2)
(otherwise デフォルト値))Code language: Lisp (lisp)
キー式を一度評価し、各節の値と eql で比較します4。
条件値は、リストでまとめて指定することもできます。otherwise はデフォルト節で、t と書いても同じ意味になります。
月の日数を返す関数を書きます。
30日・28日または29日・それ以外という3種類に振り分けるので、case のリストキーが活きます。
(defun days-in-month (m y)
(case m
((4 6 9 11) 30)
(2 (if (leap-year-p y) 29 28))
(otherwise 31)))
(days-in-month 2 2024) ;=> 29
(days-in-month 2 2025) ;=> 28Code language: Lisp (lisp)
リストキー (4 6 9 11) は「このどれかに一致したら」という意味で、値として比較するのではなく候補の列挙です5。
30日の月だけ明示して、残りを otherwise 31 で受けています。
概念的には case は cond の特殊例ですが、コンパイラはその性質を利用して最適化できます。cond は条件式を上から順に評価するので、原理的には逐次的な比較になります。
一方 case はキーが eql による整数やシンボルの一致に限定されているため、コンパイラがジャンプテーブルや二分探索に変換できます。
C言語での switch文に近い制御構造で、処理系依存ですが、節の数が増えるほど cond の逐次比較との差が出やすくなります。
3.1. caseに文字列は使えない
case が使う比較関数は eql に固定されています。
シンボルはプログラム中で唯一のオブジェクトなので、eql で正しく比較できます6。
しかし、文字列やコンスセルをキーにしても機能しません。
;; NG: 文字列キーはeqlで比較できない
(defun describe-season (name)
(case name
("spring" :warm)
("summer" :hot)
(otherwise :unknown)))
(describe-season "spring")
;=> :UNKNOWN ; 意図と違うCode language: Lisp (lisp)
文字列で分岐したいときは、condとequalを使います。
(defun describe-season/cond (name)
(cond
((equal name "spring") :warm)
((equal name "summer") :hot)
(t :unknown)))
(describe-season "spring") ;=> :WARMCode language: Lisp (lisp)
3.2. caseの t とcondの t は微妙に違う
GitHub上の実コードを見ると、case のデフォルト節には t と otherwise の両方が使われています。
読みやすさを重視するコードでは、otherwise がよく見られますが、処理系内部や短く書くスタイルのコードでは t も普通に使われています。
ただ、caseの t は、condの t と同じような見た目ですが、実際には意味が微妙に違う点は意識しておく必要があります。cond の t は「常に真の条件式」ですが、case における otherwise と t はキーとして eql で比較されるのではなく、デフォルト節の印として特別扱いされます。
そのため、 nil にもマッチしてしまいます。
;; tやotherwiseはデフォルト節として機能する
(case nil
(t :matched-t) ; これはデフォルト節、nilにマッチしても入る
(otherwise :default)) ; これもデフォルト節
;;=> :MATCHED-TCode language: Lisp (lisp)
言語仕様には、もし t や otherwise をキーとして使いたいときは (t) や (otherwise) とリストで包む、とあります。
;; tをキーとして比較したい場合はリストで包む
(case t
((t) :matched-t) ;=> :MATCHED-T
(otherwise :default))
;; nilも同様
(case nil
((nil) :matched-nil) ;=> :MATCHED-NIL
(otherwise :default))Code language: Lisp (lisp)
4. 1900-01-01からの経過日数から計算する
これを使うと、1900年1月1日が月曜(0)であることを基点にして、年のループと月のループを重ねて経過日数を出せます。
7で割った余りが曜日番号です。
(defun days-since-epoch (y m d)
;; 1900年1月1日からの経過日数
(+ (loop for yy from 1900 below y
sum (if (leap-year-p yy) 366 365))
(loop for mm from 1 below m
sum (days-in-month mm y))
(1- d)))
(defun day-index (y m d)
;; 1900年1月1日は月曜(0)
(mod (days-since-epoch y m d) 7))
(day-index 2026 5 9)
;=> 5 (土曜)Code language: Lisp (lisp)
4.1. 【補足】encode-universal-time
encode-universal-time と decode-universal-time を使うと、年月日から曜日番号を得る関数を作れます。
(defun day-index/decode (y m d)
(nth-value 6 (decode-universal-time
(encode-universal-time 0 0 0 d m y))))Code language: Lisp (lisp)
0が月曜、6が日曜です7。
5. ecaseは網羅性を強制する
ecase は case の亜種で、どの節にもマッチしなかったとき type-error を出します。
otherwise が書けないのが特徴で、言語使用(CLtL2)によると、「exhaustive case(網羅的なケース)」または「error-checking case」の略とされています。
(ecase キー式
(値1 結果1)
(値2 結果2))Code language: Lisp (lisp)
この0〜6の整数をシンボルに変換するのが weekday-symbol です。
整数とシンボルの1対1対応なので case で書けます。
(defun weekday-symbol (n)
(ecase n
(0 :monday)
(1 :tuesday)
(2 :wednesday)
(3 :thursday)
(4 :friday)
(5 :saturday)
(6 :sunday)))
(weekday-symbol 5) ;=> :SATURDAYCode language: Lisp (lisp)
ecase を使うと、対応していない値が来た瞬間にエラーになるので、節を書き忘れたときに気づけます。
エラーメッセージに「どの値が期待されていたか」が含まれます。case で書いて otherwise nil にすると、この検出ができません8。
これで全部品がそろいました。
組み合わせると完成です。
(defun leap-year-p (y)
(cond ((zerop (mod y 400)) t)
((zerop (mod y 100)) nil)
((zerop (mod y 4)) t)
(t nil)))
(defun days-in-month (m y)
(case m
((4 6 9 11) 30)
(2 (if (leap-year-p y) 29 28))
(otherwise 31)))
(defun days-since-epoch (y m d)
(+ (loop for yy from 1900 below y
sum (if (leap-year-p yy) 366 365))
(loop for mm from 1 below m
sum (days-in-month mm y))
(1- d)))
(defun day-index (y m d)
(mod (days-since-epoch y m d) 7))
(defun weekday-symbol (n)
(ecase n
(0 :monday)
(1 :tuesday)
(2 :wednesday)
(3 :thursday)
(4 :friday)
(5 :saturday)
(6 :sunday)))
(defun day-symbol (y m d)
(weekday-symbol (day-index y m d)))
(day-symbol 2026 5 9)
;=> :SATURDAYCode language: Lisp (lisp)
6. 特殊な条件分岐
6.1. typecase/etypecaseで型による分岐
型で分岐したいときは typecase を使います。
cond と typep の組み合わせと同じ結果になりますが、意図が明確になります9。
;; typecaseで書く場合
(defun describe-value (x)
(typecase x
(integer :integer)
(string :string)
(list :list)
(otherwise :unknown)))
(describe-value 42) ;=> :INTEGER
(describe-value "hello") ;=> :STRING
(describe-value '(1 2)) ;=> :LIST
;; condとtypepで書く場合
(defun describe-value/cond (x)
(cond ((typep x 'integer) :integer)
((typep x 'string) :string)
((typep x 'list) :list)
(t :unknown)))Code language: Lisp (lisp)
etypecase は ecase と同様、マッチしなかったとき type-error を出します。
6.2. ccaseで修正可能なエラーを出す
case の仲間にはもう一つ ccase があります。c は「continuable」の略で、CLtL2 では「continuable exhaustive case」と説明されています。
マッチしなかったとき修正可能なエラーを出し、デバッガから新しい値を渡してリトライできます。
(defun decode-roman (x)
(ccase x
((i uno) 1)
((ii dos) 2)
((iii tres) 3)))
(decode-roman 'iiii)
; Error: The value of X, IIII, is not I, UNO, II, DOS, III, or TRES.
; 1: Supply a value to use instead.
; Debug> :continue 1
; Use value: 'iii
;=> 3Code language: Lisp (lisp)
ecase との違いはエラーから継続できるかどうかだけです。
実用コードでは「マッチしない値はバグ」として即停止させる ecase が多く使われます。ccase は開発中に値を差し替えながら試したい場面に向きます。
7. 条件分岐の実例
7.1. condで再帰関数を書く
多くのプログラミング言語では、条件分岐の基本は if ですが、Common Lispでは、複数条件分岐の cond が「場合分け」を表現するのによく使われます。
たとえば、ネストしたリストの要素数を数える例です。
(defun count-atoms (x)
(cond ((null x) 0)
((atom x) 1)
(t (+ (count-atoms (car x))
(count-atoms (cdr x))))))
(count-atoms '(1 (2 3) (4 (5 6)))) ;=> 6Code language: Lisp (lisp)
リスト処理の再帰では null・atom・t の3分岐が典型的なパターンです。
空リスト・アトム・コンスセルの3種類に場合分けして、コンスセルのときだけ再帰します。null を先に判定しているのは、nil が atom でもあるためです10。
7.2. condの条件式の値を返す
cond の節は (条件式 値) の形が基本ですが、値を省くと条件式の値そのものが返ります。
(cond (条件式)) ; 条件式が真なら、その値が返り nil なら次の条件に進むCode language: Lisp (lisp)
「見つかればその要素、見つからなければ nil」というパターンを一行で書けます11。assoc や find と組み合わせると便利です。
(defparameter *holidays*
'(("元日" . (1 1))
("こどもの日" . (5 5))
("クリスマス" . (12 25))))
(defun find-holiday (m d)
(cond ((assoc-if (lambda (entry)
(equal (cdr entry) (list m d)))
*holidays*))))
(find-holiday 5 5) ;=> ("こどもの日" . (5 5))
(find-holiday 5 9) ;=> NILCode language: Lisp (lisp)
assoc が見つけたコンスセルをそのまま返しています。if で書くと同じことを2行に分けて書く必要があるので、簡潔に書けます。
7.3. ディスパッチテーブルへのリファクタリング
case の節が増えてくると、関数の見通しが悪くなります。
;; caseで書いた場合
(defun http-status-message (code)
(case code
(200 "OK")
(201 "Created")
(400 "Bad Request")
(401 "Unauthorized")
(403 "Forbidden")
(404 "Not Found")
(500 "Internal Server Error")
(otherwise "Unknown")))Code language: Lisp (lisp)
節の追加・削除をデータとして管理したい場合は、ハッシュテーブルに切り替えると保守しやすくなります。
;; ハッシュテーブルに切り替えた場合
(defparameter *http-messages*
(let ((table (make-hash-table)))
(loop for (code . message) in '((200 . "OK")
(201 . "Created")
(400 . "Bad Request")
(401 . "Unauthorized")
(403 . "Forbidden")
(404 . "Not Found")
(500 . "Internal Server Error"))
do (setf (gethash code table) message))
table))
(defun http-status-message (code)
(gethash code *http-messages* "Unknown"))
(http-status-message 404) ;=> "Not Found"
(http-status-message 999) ;=> "Unknown"Code language: Lisp (lisp)
ハッシュテーブルにすると、エントリの追加・削除がデータの操作になります。
関数を書き換えずに対応を増やせるので、外部ファイルや設定から動的に読み込む用途にも向きます。
ただ、小規模な対応表なら case の方が読みやすく、エントリ数と変更頻度で使い分けが決まります12。
- CLtL2 では
ecaseとetypecaseが導入された経緯として「many programmers are too lazy to put an appropriate otherwise clause into every case statement」と述べられています。ecaseはその怠慢を防ぐために標準に含められました。 – CLtL2: Special Forms for Exhaustive Case Analysis - HyperSpec では「If no test-form yields true, nil is returned」と定義されています。節に値を書かない場合は条件式の値そのものが返ります。 – CLHS: Macro COND
- グレゴリオ暦のうるう年規則は「4で割り切れる年はうるう年。ただし100で割り切れる年は平年。ただし400で割り切れる年はうるう年」という優先順位で成り立っています。Common Lisp の言語仕様(CLtL2)でも同じ規則が明示されています。 – CLtL2: Time Functions
- HyperSpec では「These macros allow the conditional execution of a body of forms in a clause that is selected by matching the test-key on the basis of its identity」と説明されており、比較は同一性(identity)、つまり
eqlによって行われます。 – CLHS: Macro CASE, CCASE, ECASE - HyperSpec のサンプルコードでも
((1 2) 'clause1)のように複数キーをリストで並べる書き方が示されています。リストそのものとの比較ではなく、候補値の列挙として扱われます。 – CLHS: Macro CASE, CCASE, ECASE eqlは数値・文字・シンボルについては値の同一性を比較しますが、文字列は同じ内容でも別オブジェクトとして扱われるためeqlでは一致しません。文字列の内容比較にはequalを使います。 – CLHS: Function EQLdecode-universal-timeは9つの値を返します。順に second、minute、hour、day、month、year、day-of-week、daylight-saving-time-p、time-zone で、曜日番号は第7戻り値(0始まり)です。0が月曜、6が日曜という定義はHyperSpecで規定されています。1900年1月1日以前の日付は処理できません。 – The Common Lisp Cookbook – Dates and Times- HyperSpec には「If no normal-clause matches, a non-correctable error of type type-error is signaled. The offending datum is the test-key and the expected type is type equivalent to (member key1 key2 …)」と定義されています。
ccaseは修正可能なエラーを出しますが、ecaseは修正不可能なエラーを出す点が異なります。 – CLHS: Macro CASE, CCASE, ECASE - HyperSpec では
typecaseの展開形として(let ((var test-key)) (cond ((typep var 'type1) ...) ...))と等価であることが明示されています。 – CLHS: Macro TYPECASE, CTYPECASE, ETYPECASE - HyperSpec では
(atom nil) => trueと明示されています。atomは「consでないもの」を真とする述語なので、空リストであるnilもアトムとして扱われます。 – CLHS: Function ATOM - HyperSpec の
condの定義には「If there are no forms in that clause, the primary value of the test-form is returned by the cond form」と明記されています。 – CLHS: Macro COND - ハッシュテーブルの詳細な使い方(make-hash-table、gethash、走査方法など)については別記事で解説しています。 – 【Common Lisp】ハッシュテーブルの基本の使い方(make-hash-table)