Clojure

Clojure入門 - 制御フロー

文と式

Javaでは、式は値を返し、文は値を返しません。

// "if" is a statement because it doesn't return a value:
String s;
if (x > 10) {
    s = "greater";
} else {
    s = "less or equal";
}
obj.someMethod(s);

// Ternary operator is an expression; it returns a value:
obj.someMethod(x > 10 ? "greater" : "less 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 is optional
nil

真偽値

Clojureでは、すべての値は論理的に真または偽です。"偽"の値はfalsenilのみであり、他のすべての値は論理的に真です。

user=> (if true :truthy :falsey)
:truthy
user=> (if (Object.) :truthy :falsey) ; objects are true
:truthy
user=> (if [] :truthy :falsey) ; empty collections are true
:truthy
user=> (if 0 :truthy :falsey) ; zero is true
:truthy
user=> (if false :truthy :falsey)
:falsey
user=> (if nil :truthy :falsey)
:falsey

ifdo

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"))

condelse

テストがどれも満たされていない場合、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

caseelse

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)] ; list of 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)] ; list of 0, 1, 2
         [letter number])
([:a 0] [:a 1] [:a 2] [:b 0] [:b 1] [:b 2])

再帰

再帰と反復処理

  • Clojureはrecurとシーケンス抽象化を提供します。

  • recurは「古典的な」再帰です。

    • コンシューマはそれを制御せず、低レベルの機能と見なされます。

  • シーケンスは反復処理を値として表します。

    • コンシューマは部分的に反復処理できます。

  • リデューサは反復処理を関数合成として表します。

    • Clojure 1.5で追加されましたが、ここでは説明しません。

looprecur

  • 関数型ループ構成です。

    • loopはバインディングを定義します。

    • recurは新しいバインディングでloopを再実行します。

  • 代わりに高階関数ライブラリ関数を使用することをお勧めします。

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

defnrecur

  • 関数引数は暗黙的なloopバインディングです。

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

再帰のためのrecur

  • recurは「末尾位置」になければなりません。

    • 分岐の最後の式です。

  • recurは、位置によってすべてのバインドされたシンボルに値を提供する必要があります。

    • ループバインディング

    • 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))))

;; Can be written:
(with-open [f (clojure.java.io/writer "/tmp/new")]
  (.write f "some text"))