cond / case / ecase icon Branching tree representing Common Lisp conditional forms 【Common Lisp】
複数条件分岐の基本の使い方
(cond / case / ecase)

  • condは任意の条件式を並べる汎用の分岐、
    caseeqlによる値一致に特化した簡潔な分岐、
    ecaseはマッチしない値を即座にエラーにする網羅強制版という階層関係にあります。
  • caseのキーにはリストで複数値をまとめて指定でき、otherwiseでデフォルト節を書けますが、文字列やコンスセルはeqlで比較できないため使えません。
  • 年月日から曜日を求める実装を通じて、うるう年判定にcond、月の日数取得にcase、曜日番号とシンボルの対応にecaseを使い分ける具体例を示しています。

関連記事

1. 問題:日付から曜日を求める

年月日から曜日名を返す問題を解いていく中で、condcaseecase の使い方を整理していきます。

完成形はこうです。

(day-symbol 2026 5 9)
;=> :SATURDAYCode language: Lisp (lisp)

1.1. condとcaseとecaseの関係

  • cond は最も汎用で、任意の条件式を並べられます。
  • 条件が「1つの式とさまざまな値の一致」に限定されるなら case で簡潔に書けます。
  • さらに、全ケースを網羅することが仕様上保証されるなら ecase にすると、想定外の値を即座にエラー検出できます。
条件式比較マッチなし
cond任意の式任意nil(tでデフォルト値を指定できる)
caseeql 一致eqlnilt/otherwise でデフォルト値を指定できる)
ecaseeql 一致eqltype-error
ccaseeql 一致eql修正可能エラー
typecasetype 一致typepnilt/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)

caseecase の違いはマッチしなかったときの挙動だけです。
「マッチしない値が来ることがありうる」なら caseotherwise、「マッチしない値は仕様上ありえない」なら ecase という使い分けになります1
比較関数は同じ eql で、キーに使える型も変わりません。

2. condで任意の条件を並べる

cond の基本形はこうです。

(cond (条件式11)
      (条件式22)
      (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 で受けています。

概念的には casecond の特殊例ですが、コンパイラはその性質を利用して最適化できます。
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 も普通に使われています。

ただ、caset は、condt と同じような見た目ですが、実際には意味が微妙に違う点は意識しておく必要があります。
condt は「常に真の条件式」ですが、case における otherwiset はキーとして eql で比較されるのではなく、デフォルト節の印として特別扱いされます。

そのため、 nil にもマッチしてしまいます。

;; tやotherwiseはデフォルト節として機能する
(case nil
  (t   :matched-t)         ; これはデフォルト節、nilにマッチしても入る
  (otherwise :default))    ; これもデフォルト節
;;=> :MATCHED-TCode language: Lisp (lisp)

言語仕様には、もし totherwise をキーとして使いたいときは (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-timedecode-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は網羅性を強制する

ecasecase の亜種で、どの節にもマッチしなかったとき 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 を使います。

condtypep の組み合わせと同じ結果になりますが、意図が明確になります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)

etypecaseecase と同様、マッチしなかったとき 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)

リスト処理の再帰では nullatomt の3分岐が典型的なパターンです。
空リスト・アトム・コンスセルの3種類に場合分けして、コンスセルのときだけ再帰します。
null を先に判定しているのは、nilatom でもあるためです10

7.2. condの条件式の値を返す

cond の節は (条件式 値) の形が基本ですが、値を省くと条件式の値そのものが返ります。

(cond (条件式))  ; 条件式が真なら、その値が返り nil なら次の条件に進むCode language: Lisp (lisp)

「見つかればその要素、見つからなければ nil」というパターンを一行で書けます11
assocfind と組み合わせると便利です。

(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

  1. CLtL2 では ecaseetypecase が導入された経緯として「many programmers are too lazy to put an appropriate otherwise clause into every case statement」と述べられています。ecase はその怠慢を防ぐために標準に含められました。 – CLtL2: Special Forms for Exhaustive Case Analysis
  2. HyperSpec では「If no test-form yields true, nil is returned」と定義されています。節に値を書かない場合は条件式の値そのものが返ります。 – CLHS: Macro COND
  3. グレゴリオ暦のうるう年規則は「4で割り切れる年はうるう年。ただし100で割り切れる年は平年。ただし400で割り切れる年はうるう年」という優先順位で成り立っています。Common Lisp の言語仕様(CLtL2)でも同じ規則が明示されています。 – CLtL2: Time Functions
  4. 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
  5. HyperSpec のサンプルコードでも ((1 2) 'clause1) のように複数キーをリストで並べる書き方が示されています。リストそのものとの比較ではなく、候補値の列挙として扱われます。 – CLHS: Macro CASE, CCASE, ECASE
  6. eql は数値・文字・シンボルについては値の同一性を比較しますが、文字列は同じ内容でも別オブジェクトとして扱われるため eql では一致しません。文字列の内容比較には equal を使います。 – CLHS: Function EQL
  7. decode-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
  8. 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
  9. HyperSpec では typecase の展開形として (let ((var test-key)) (cond ((typep var 'type1) ...) ...)) と等価であることが明示されています。 – CLHS: Macro TYPECASE, CTYPECASE, ETYPECASE
  10. HyperSpec では (atom nil) => true と明示されています。atom は「consでないもの」を真とする述語なので、空リストである nil もアトムとして扱われます。 – CLHS: Function ATOM
  11. 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
  12. ハッシュテーブルの詳細な使い方(make-hash-table、gethash、走査方法など)については別記事で解説しています。 – 【Common Lisp】ハッシュテーブルの基本の使い方(make-hash-table)