CLOVER🍀

That was when it all began.

Clojureの条件分岐を覚える

ifとかwhenとかcaseとかいろいろあって、いつも迷うので1度しっかり見てみることにしました。代表的そうなものを、以下順に。

if

最も単純な条件分岐です。最初に条件式を書き、次に条件がtrueとなった場合の処理、その次にelseの処理を書きます。戻り値は、それぞれのケースで評価された値になります。

;; if
(defn if-test
  [n]
  (if (= 1 n)
    "True!"
    "False!"))

(println (if-test 1))  ;; => True!
(println (if-test 2))  ;; => False!

else if的なものはなさそうですね。その場合は、後述のcaseなどを使用します。あと、評価が逆になるif-notもあります。

なお、評価後の値に括弧が無いように、そのままでは複数の式を書くことはできません。その場合は、do特殊形式を入れる必要があります。
例えば、こんな感じ。

(println
 (if true
   (do (print "Hello World ") "True!")
   "False!"))  ;; => Hello World True!

if-let

ifとletの合わせ技のようです。if-letで評価した値がtrueと判定されるようなら、そのまま変数に束縛できるようですね。

;; if-let
(defn if-let-test
  [coll]
  (if-let [c (seq (filter even? coll))]
    c
    "Cond is False"))

(println (if-let-test [1 2 3 4]))  ;; (2 4)
(println (if-let-test [1 3 5 7]))  ;; Cond is False

条件がtrueにならなかった場合は、if同様にelseの値が返ります。

when

ifに似ていますが、elseがありません。また、後続の処理は暗黙的にdo特殊形式内で実行されることになります。
よって、ifと異なり複数の式を書く際に、do特殊形式を付与する必要がありません。

;; when
(defn when-test
  [n]
  (when (= 1 n)
    (print "When")
    (print " ")
    (print "True!")
    (print " ")
    n))

(println (when-test 1))  ;; => "When True!" 1
(println (when-test 2))  ;; => nil

条件式がtrueにならなかった場合は、nilが返ります。こちらも、対となるwhen-notがあります。

when-let

if-letのwhenを使用する版です。条件がtrueになる場合に、変数に値を束縛できる以外は、whenと同じ特徴を持ちます。

;; when-let
(defn when-let-test
  [coll]
  (when-let [c (seq coll)]
    (print "When-let")
    (print " ")
    (print c)
    (print " ")
    (print "True!")
    (print " ")
    c))

(println (when-let-test [1 2 3]))  ;; => When-let (1 2 3) True! (1 2 3)
(println (when-let-test []))  ;; => nil

case

ここから、複数の条件を扱うものになります。まずはcase。

caseの次に指定した値と、以降に比較する値、結果値のペアを順に並べていきます。比較は=で行われる?いずれの条件にもマッチしなかった場合については、デフォルト値を単独で書きます。

;; case
(defn case-test-1
  [n]
  (case n
        1 "n is 1"
        2 "n is 2"
        "n is other"))

(println (case-test-1 1))  ;; n is 1
(println (case-test-1 2))  ;; n is 2
(println (case-test-1 3))  ;; n is other

なお、デフォルトケースを用意せずにいずれの条件にもマッチしなかった場合は、IllegalArgumentExceptionがスローされます。

(defn case-test-2
  [v]
  (case v
        [] "v is empty"
        [1 2] "v is [1 2]"))

(println (case-test-2 []))  ;; v is empty
(println (case-test-2 [1 2]))  ;; v is [1 2]
(println (try (case-test-2 [1 2 3])
              (catch IllegalArgumentException e (.toString e))))  ;; java.lang.IllegalArgumentException: No matching clause: [1 2 3]

(println (case 3
               (1 2 3) "1, 2, or 3"
               "default"))  ;; 1, 2, or 3

cond

caseより、もう少し汎用的な使い方ができます。caseのように比較対象の値を指定するわけでなく、条件式そのものを並べていきます。条件式がtrueになった場合の結果値は、caseと同様すぐ次に書きます。
なお、デフォルトケースは:elseで指定します。

;; cond
(defn cond-test
  [n]
  (cond
   (= n 1) "n is 1"
   (= n 2) "n is 2"
   (and (> n 3) (< n 10)) "n is over 3 and under 10"
   :else "n is other"))

(println (cond-test 1))  ;; n is 1
(println (cond-test 2))  ;; n is 2
(println (cond-test 5))  ;; n is over 3 and under 10
(println (cond-test 15))  ;; n is other

デフォルトケースを定義せず、いずれの条件にもマッチしなかった場合は、nilが返ります。

(println (let [n 1]
           (cond
            (= n 2) "two"
            (= n 3) "three"))) ;; => nil

condp

condの変形?condp自身は、condを使って書かれているようですが…。
最初に、述語と比較対象の値を取ります。その後の定義方法は、caseと同じです。

;; condp
(defn condp-test-1
  [n]
  (condp = n
    1 "n is 1"
    2 "n is 2"
    "n is other"))

(println (condp-test-1 1))  ;; n is 1
(println (condp-test-1 2))  ;; n is 2
(println (condp-test-1 10))  ;; n is other

述語を取るので、もう少し変わった使い方もできるようです。

(defn condp-test-2
  [coll]
  (condp some coll
    #{1 2 3} "coll contains 1 or 2 or 3"
    #{4 5 6} "coll contains 4 or 5 or 6"
    "coll is other"))

(println (condp-test-2 [1 2 3 4]))  ;; coll contains 1 or 2 or 3
(println (condp-test-2 [4]))  ;; coll contains 4 or 5 or 6
(println (condp-test-2 [7 8 9]))  ;; coll is other

動作を見る限り、例えば最初の例は

(println (some #{1 2 3} [1 2 3 4]))

のような形で実行されているっぽいですね。

その他、:>>を使用することでマッチした値を引数に取る関数を実行させることもできます。

(defn condp-test-3
  [coll]
  (condp some coll
    #{1 2 3} :>> #(* 10 %)
    #{4 5 6} :>> dec))

(println (condp-test-3 [1 2 3]))  ;; 10
(println (condp-test-3 [8 7 6]))  ;; 5

最初の例は、1がマッチするのでそれを10倍にしています。次の例は、6がマッチするのでデクリメントして返却しています。

condpの場合は、デフォルトケースを定義せずにいずれの条件式にもマッチしなかった場合は、IllegalArgumentExceptionがスローされます。

(println (try (condp-test-3 [10 11 12])
              (catch IllegalArgumentException e (.toString e))))  ;; java.lang.IllegalArgumentException: No matching clause: [10 11 12]