(def hello (fn [] "Hello world"))
-> #'user/hello
(hello)
-> "Hello world"
Clojureは関数型プログラミング言語だ。Clojureは、ミュータブルな状態を避けるためのツールを提供し、ファーストクラスオブジェクトとしての関数を提供し、副作用に基づくループの代わりに再帰的な繰り返しに重きを置いている。Clojureは、プログラムが参照透過であることを強制しないし「証明可能」なプログラムを目指しているわけでもないという点で 非純粋(impure) だ。Clojureの背後にある哲学は、ほとんどのプログラムのほとんどの部分は関数型であるべきであり、プログラムが関数型であればあるほど堅牢になる、というものだ。
fn は関数オブジェクトを作る。他のものと同様に値を作り出す――varに格納したり関数に渡したりなどできる。
(def hello (fn [] "Hello world"))
-> #'user/hello
(hello)
-> "Hello world"
defn は関数定義を少しシンプルにしてくれるマクロだ。Clojureは、 単一の 関数オブジェクトでのアリティオーバーロード、自己参照、 & による可変アリティ関数をサポートしている:
;でっち上げの例
(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はイミュータブルなリスト、ベクター、セット、マップを提供している。それらは変更できないため、イミュータブルなコレクションから「追加」したり「削除」したりするのは、ちょうど古いコレクションに必要な変更を加えたような新しいコレクションを作ることを意味する。 永続性(persistence) とは「変更」後にもコレクションの古いバージョンを得ることができ、たいていの操作についてパフォーマンスを保証する性質のことをいう用語だ。特に、これは新しいバージョンが完全なコピーを使って作られることはないということを意味する、というのもそれには線形時間を必要とするからだ。必然的に、永続的なコレクションは連結データ構造で実装され、それにより新しいバージョンは以前のバージョンと構造を共有することができる。単方向連結リストとツリーが基本的な関数型データ構造で、Clojureはこれにarray mapped hash trieをもとにしたハッシュマップ、セット、ベクターを追加している。コレクションは読みやすい表現と共通のインターフェースを持っている:
(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)
;オリジナルはそのまま
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のすべてのデータ構造はseqを提供することができる。seqは他の言語におけるイテレータやジェネレータのように利用できるが、seqにはイミュータブルで永続的だという重要な利点がある。seqは極めてシンプルで、シーケンスの最初の要素を返す first 関数と、シーケンスの残り(それ自身がseqまたはnilのどちらか)を返す rest 関数を提供している。
(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のライブラリ関数の多くはseqを 遅延評価 で生み出したり取り込んだりする:
;cycleは「無限」の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 マクロを利用することで独自の遅延シーケンスを生み出す関数を定義することができる。lazy-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特殊オペレータ を提供し、これによって再束縛と最も近いloopまたは関数フレームへのジャンプによる定数空間での再帰ループを行う。末尾呼び出し最適化ほど一般的なものではないが、同様にエレガントな構造のほとんどを可能にし、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 が良い選択肢になるかもしれない。