文 vs. 式

Javaでは、式は値を返すが、文は値を返さない。

// "if"は値を返さないので文だ:
String s;
if (x > 10) {
    s = "greater";
} else {
    s = "greater or equal";
}
obj.someMethod(s);

// 三項演算子は式だ。なぜなら値を返すからだ。:
obj.someMethod(x > 10 ? "greater" : "greater or equal");

しかしClojureでは、あらゆるものが式なのだ! あらゆるもの が値を返し、複数の式のブロックは最後の値を返す。副作用のみを実行する式は nil を返す。

フロー制御式

したがって、フロー制御のオペレータもまた式なのだ!

フロー制御のオペレータは組み合わせ可能なので、どこでも使うことができる。これにより、重複コードが減り、中間変数が少なくなる。

フロー制御のオペレータはマクロによって拡張することもできる。マクロはユーザのコードによってコンパイラを拡張することを可能にする。今日はマクロについて議論しないが、詳しくは マクロ 、 Clojure from the Ground Up 、 Clojure for the Brave and True ほか多くの場所で読むことができる。

if

if は最も重要な制御式であり、条件と"then"と"else"で構成されている。 if は条件によって選ばれた分岐だけを評価する。

user=> (str "2 is " (if (even? 2) "even" "odd"))
2 is even

user=> (if (true? false) "impossible!") ;; elseは省くこともできる
nil

真

Clojureでは、すべての値は論理的に真または偽のどちらかだ。 false と nil の値だけが「偽」であり、その他のすべての値は論理的に真だ。

user=> (if true :truthy :falsey)
:truthy
user=> (if (Object.) :truthy :falsey) ; オブジェクトは真
:truthy
user=> (if [] :truthy :falsey) ; 空のコレクションは真
:truthy
user=> (if 0 :truthy :falsey) ; 0は真
:truthy
user=> (if false :truthy :falsey)
:falsey
user=> (if nil :truthy :falsey)
:falsey

if と do

if は"then"と"else"に単一の式しかとらない。単一の式よりも大きなブロックを作るには do を使おう。

このようにする唯一の理由は本体部が副作用を持つからであることに注意しよう! (なぜか分かるだろうか?)

(if (even? 5)
  (do (println "even")
      true)
  (do (println "odd")
      false))

when

when は then の分岐だけを持つ if だ。条件をチェックし、本体部として任意個の文を評価する(なので do は必要ない)。 最後の式の値が返る。条件が偽の場合、 nilが返る。

when は読み手に"else"の分岐がないことを伝える。

(when (neg? x)
  (throw (RuntimeException. (str "x must be positive: " x))))

cond

cond はテストと式が並んだものだ。個々のテストは順に評価され、最初に真になるテストに対応する式が評価されて返る。

(let [x 5]
  (cond
    (< x 2) "x is less than 2"
    (< x 10) "x is less than 10"))

cond と else

いずれのテストも満たさない場合、nilが返る。最後のテストに :else を使うのが一般的なイディオムだ。 :else のようなキーワードは常に真と評価されるため、これが常にデフォルトとして選ばれる。

(let [x 11]
  (cond
    (< x 2)  "x is less than 2"
    (< x 10) "x is less than 10"
    :else  "x is greater than or equal to 10"))

case

case は引数を一連の値と比較し一致するものを探す。これは(線形ではなく)定数時間で終わる! しかし、個々の値はコンパイル時リテラル(数値、文字列、キーワードなど)でなければならない。

cond, case と違い、いずれの値にも一致しないと例外をスローする。

user=> (defn foo [x]
         (case x
           5 "x is 5"
           10 "x is 10"))
#'user/foo

user=> (foo 10)
x is 10

user=> (foo 11)
IllegalArgumentException No matching clause: 11

else 式付きの case

case は最後にひとつの式を持つことができ、いずれのテストも満たさない場合に評価される。

user=> (defn foo [x]
         (case x
           5 "x is 5"
           10 "x is 10"
           "x isn't 5 or 10"))
#'user/foo

user=> (foo 11)
x isn't 5 or 10

副作用のための繰り返し

dotimes

  • 式を n 回評価する

  • nil を返す

user=> (dotimes [i 3]
         (println i))
0
1
2
nil

doseq

  • シーケンスに対して繰り返す

  • 遅延シーケンスの場合、評価を強制する

  • nil を返す

user=> (doseq [n (range 3)]
         (println n))
0
1
2
nil

複数の束縛を持った doseq

  • ネストした foreach のループに似ている

  • シーケンスの中身のすべての順列を処理する

  • nil を返す

user=> (doseq [letter [:a :b]
               number (range 3)] ; 0, 1, 2の要素を持つリスト
         (prn [letter number]))
[:a 0]
[:a 1]
[:a 2]
[:b 0]
[:b 1]
[:b 2]
nil

Clojureの for

  • forループ ではなく リスト内包表記

  • シーケンスを順に取り出すためのジェネレータ関数

  • 束縛は doseq と同じように振る舞う

user=> (for [letter [:a :b]
             number (range 3)] ; 0, 1, 2の要素を持つリスト
         [letter number])
([:a 0] [:a 1] [:a 2] [:b 0] [:b 1] [:b 2])

再帰

再帰と繰り返し

  • Clojureはrecurとシーケンス抽象を提供している

  • recur は「古典的な」再帰だ

    • 利用者がコントロールできず、低レベルな機能と考えられる

  • シーケンスは繰り返しを値として表現する

    • 利用者が部分的に繰り返すことができる

  • reducerは繰り返しを関数合成として表現する

    • Clojure 1.5で追加されたが、ここでは扱わない

loop と recur

  • 関数型のループ構文

    • loop は束縛を定義する

    • recur は新たな束縛で loop を再実行する

  • むしろライブラリの高階関数を使うようにしよう

(loop [i 0]
  (if (< i 10)
    (recur (inc i))
    i))

defn と recur

  • 関数の引数は暗黙的な loop の束縛だ

(defn increase [i]
  (if (< i 10)
    (recur (inc i))
    i))

再帰のための recur

  • recur は「末尾位置」になければならない式

    • 分岐の最後の式

  • recur はすべての束縛シンボルに対応する値を与えなければならない数

    • loopの束縛

    • defn/fnの引数

  • recur による再帰はスタックを消費しない

例外

例外処理

  • Java と同じような try/catch/finally

(try
  (/ 2 1)
  (catch ArithmeticException e
    "divide by zero")
  (finally
    (println "cleanup")))

例外をスローする

(try
  (throw (Exception. "something went wrong"))
  (catch Exception e (.getMessage e)))

Clojureのデータを持った例外

  • ex-info はメッセージとマップをとる

  • ex-data はそのマップを取り出す

    • もしくは ex-info で作られたものでなければ nil

(try
  (throw (ex-info "There was a problem" {:detail 42}))
  (catch Exception e
    (prn (:detail (ex-data e)))))

with-open

(let [f (clojure.java.io/writer "/tmp/new")]
  (try
    (.write f "some text")
    (finally
      (.close f))))

;; このように書ける:
(with-open [f (clojure.java.io/writer "/tmp/new")]
  (.write f "some text"))