(def hello (fn [] "Hello world"))
-> #'user/hello
(hello)
-> "Hello world"
Clojureは関数型プログラミング言語です。可変状態を回避するためのツールを提供し、関数をファーストクラスオブジェクトとして提供し、副作用に基づいたループの代わりに再帰的なイテレーションを重視します。Clojureは、プログラムを参照透過的にすることを強制せず、「証明可能な」プログラムを目指さないという意味で非純粋です。Clojureの背後にある哲学は、ほとんどのプログラムのほとんどの部分は関数型であるべきであり、より関数型であるプログラムはより堅牢であるということです。
fn は関数オブジェクトを作成します。他の値と同様に値を生成し、変数に格納したり、関数に渡したりできます。
(def hello (fn [] "Hello world"))
-> #'user/hello
(hello)
-> "Hello world"
defn は関数定義を少し簡単にするマクロです。Clojureは、単一の関数オブジェクトでのアリティオーバーロード、自己参照、および & を使用した可変長引数関数をサポートします。
;trumped-up example
(defn argcount
([] 0)
([x] 1)
([x y] 2)
([x y & more] (+ (argcount x y) (count more))))
-> #'user/argcount
(argcount)
-> 0
(argcount 1)
-> 1
(argcount 1 2)
-> 2
(argcount 1 2 3 4 5)
-> 5
let を使用して、関数内で値のローカル名を作成できます。ローカル名のスコープはレキシカルなので、ローカル名のスコープ内で作成された関数はそれらの値をクロージャします。
(defn make-adder [x]
(let [y x]
(fn [z] (+ y z))))
(def add2 (make-adder 2))
(add2 4)
-> 6
let で作成されたローカルは変数ではありません。作成されたら、それらの値は決して変わりません!
状態の変更を回避する最も簡単な方法は、イミュータブルなデータ構造を使用することです。Clojureは、イミュータブルなリスト、ベクター、セット、マップのセットを提供します。変更できないため、イミュータブルなコレクションから何かを「追加」または「削除」することは、必要な変更を加えた古いコレクションとまったく同じ新しいコレクションを作成することを意味します。永続性は、「変更」後もコレクションの古いバージョンが引き続き利用可能であり、コレクションがほとんどの操作でパフォーマンス保証を維持するプロパティを表すために使用される用語です。具体的には、これは、新しいバージョンが完全なコピーを使用して作成できないことを意味します。なぜなら、それには線形時間が必要になるからです。必然的に、永続的なコレクションは、新しいバージョンが以前のバージョンと構造を共有できるように、リンクされたデータ構造を使用して実装されます。単方向リンクリストとツリーは基本的な関数型データ構造であり、Clojureはそれに、配列マップハッシュトライに基づいたハッシュマップ、セット、およびベクターの両方を追加します。コレクションには読みやすい表現と共通のインターフェースがあります。
(let [my-vector [1 2 3 4]
my-map {:fred "ethel"}
my-list (list 4 3 2 1)]
(list
(conj my-vector 5)
(assoc my-map :ricky "lucy")
(conj my-list 5)
;the originals are intact
my-vector
my-map
my-list))
-> ([1 2 3 4 5] {:ricky "lucy", :fred "ethel"} (5 4 3 2 1) [1 2 3 4] {:fred "ethel"} (4 3 2 1))
アプリケーションでは、データの論理値とは直交するデータに関する属性やその他のデータを関連付ける必要があることがよくあります。Clojureは、このメタデータを直接サポートしています。シンボルとすべてのコレクションは、メタデータマップをサポートしています。これは、meta 関数でアクセスできます。メタデータは等価性のセマンティクスに影響を与えず、コレクションの値に対する操作ではメタデータは表示されません。メタデータは読み取り可能であり、印刷できます。
(def v [1 2 3])
(def attributed-v (with-meta v {:source :trusted}))
(:source (meta attributed-v))
-> :trusted
(= v attributed-v)
-> true
Clojureは、Javaインターフェースを使用して、コアデータ構造を定義します。これにより、これらのインターフェースの新しい具体的な実装に対するClojureの拡張が可能になり、ライブラリ関数はこれらの拡張機能で動作します。これは、言語をそのデータ型の具体的な実装にハードワイヤリングすることと比較して大きな改善です。
この良い例は、seq インターフェースです。コアLispリスト構造を抽象化にすることで、シーケンシャルインターフェースをそのコンテンツに提供できるデータ構造に、豊富なライブラリ関数が拡張されます。Clojureのすべてのデータ構造は、seqsを提供できます。seqsは、他の言語のイテレーターまたはジェネレーターのように使用できますが、seqsはイミュータブルで永続的であるという大きな利点があります。seqsは非常にシンプルで、シーケンスの最初の項目を返すfirst関数と、シーケンスの残りを返すrest関数を提供します。残りはseqまたはnilです。
(let [my-vector [1 2 3 4]
my-map {:fred "ethel" :ricky "lucy"}
my-list (list 4 3 2 1)]
[(first my-vector)
(rest my-vector)
(keys my-map)
(vals my-map)
(first my-list)
(rest my-list)])
-> [1 (2 3 4) (:ricky :fred) ("lucy" "ethel") 4 (3 2 1)]
Clojureライブラリ関数の多くは、seqsを遅延的に生成および消費します。
;cycle produces an 'infinite' seq!
(take 15 (cycle [1 2 3 4]))
-> (1 2 3 4 1 2 3 4 1 2 3 4 1 2 3)
lazy-seq マクロを使用して、独自の遅延seq生成関数を定義できます。このマクロは、0個以上の項目のリストを生成するために要求に応じて呼び出される式を取り込みます。これが簡略化されたtakeです。
(defn take [n coll]
(lazy-seq
(when (pos? n)
(when-let [s (seq coll)]
(cons (first s) (take (dec n) (rest s)))))))
可変ローカル変数が存在しない場合、ループと反復は、状態の変更によって制御される組み込みのforまたはwhile構造を持つ言語とは異なる形式をとる必要があります。関数型言語では、ループと反復は、再帰的な関数呼び出しを介して置換/実装されます。このような言語の多くは、末尾位置で行われた関数呼び出しがスタック領域を消費しないことを保証するため、再帰ループは定数スペースを利用します。ClojureはJava呼び出し規約を使用しているため、同じ末尾呼び出し最適化の保証を行うことはできず、また行いません。代わりに、recur特殊演算子を提供します。これは、定数スペースの再帰ループを、最も近い外側のループまたは関数フレームに再バインドしてジャンプすることによって行います。末尾呼び出し最適化ほど一般的ではありませんが、ほとんど同じエレガントな構造が可能になり、recurへの呼び出しが末尾位置でのみ発生する可能性があることを確認できるという利点があります。
(defn my-zipmap [keys vals]
(loop [my-map {}
my-keys (seq keys)
my-vals (seq vals)]
(if (and my-keys my-vals)
(recur (assoc my-map (first my-keys) (first my-vals))
(next my-keys)
(next my-vals))
my-map)))
(my-zipmap [:a :b :c] [1 2 3])
-> {:b 2, :c 3, :a 1}
相互再帰が必要な場合は、recurを使用できません。代わりに、trampoline が適切なオプションになる可能性があります。