Clojure のエレガントなところ

  • リスト、ハッシュマップ、ベクタ、集合のリテラルがある
;リスト
(1 2 3 4)
;ハッシュマップ
{:a 1, :b 2, :c 3}
;ベクタ
[1 2 3 4]
;集合
#{1 2 3 4}

よく使う基本データ型のリテラルがあるというのはソースコードに図が入ってるみたいで考えるよりも早く理解できる。しかもいじりやすい。他の多くの言語でもこれらの基本データ型のリテラルは用意されているが、Clojure ではこれらはとてもよく使う重要なビルディングブロック。

  • リスト、ハッシュマップ、ベクタ、集合、文字列 をシーケンスとして抽象化
(take 2 '(a b c d)) ;リスト
;=> (a b)
(take 2 {:a 1 :b 2 :c 3 :d 4})  ;ハッシュマップ
;=> ([:a 1] [:b 2])
(take 2 [:a :b :c :d]) ;ベクタ
;=> (:a :b)
(take 2 #{:a :b :c :d}) ;集合
;=> (:a :c)
(take 2 "abcd") ;文字列
;=> (\a \b)

それぞれは特定の実装であって、全てはシーケンス。ひとつのデータ型に対する多くの関数を利用できる。公理は少ない方がいい

  • シーケンス処理

Clojureの心臓はシーケンス処理だ。シーケンスがClojureを貫く共通言語なので、多くの関数同士がシーケンスを介してコミュニケーションが取れる。シーケンス処理を次々とチェインして自在に組み合わせることができるのだ。いろいろなオブジェクトの使い方をまばらに覚える代わりに、map, reduce, filter など洗練されたシーケンス処理の語彙を覚えるだけで驚くほど多様な処理ができる。

  • 多くの関数が遅延シーケンスを返す

副作用のある関数を使うと遅延評価されていることが分かりやすい。

;遅延評価(必要になったときに初めて値が評価される)
user> (map println "abcde")
(a
b
nil c
nil d
nil e
nil nil)
; nil は println の返り値
;強制的に評価する(必要になる前にすべてを評価する)
user> (doall (map println "abcde"))
a
b
c
d
e
(nil nil nil nil nil)

関数型は直截的な記述ができる一方、引数が全て評価されてから関数に渡されるため引数の大きさ分のコスト(主にメモリ)がかかる。命令的なコードでは時間さえ掛ければ解けるような問題でも関数的な記述ではメモリの制限で解けない事もある。それを解決するのが遅延シーケンス。実際に値が必要になるまで何もしない。関数型の直截的な記述と省メモリのいいとこ取り。他の lisp でも遅延シーケンスは使えるが、Clojure程容易には使えないと思う。特に何も考えなくても多くの関数が遅延シーケンスを返してくれる。更に、遅延シーケンスの管理上のコストすら削減しようと試みているらしい。Clojure 1.1 効率のためにトランジェントとチャンクシーケンスを追加

  • 変更不可(イミュータブル)のデータコレクション

リスト、ベクタ、ハッシュマップ、集合、キューなど Clojure のコレクションは一度作成したら変更不可になっている。これは関数型プログラミングと並行処理という大きな2つの点で重要になっている。どちらにも共通して、変更不可性は「予防は治療に勝る」的な利点がある。関数に与えた引数が絶対に変更されていないと確信が持てるというのは厄介なバグを防ぎ精神衛上もよい。
データは変更できないと使いものにならないが、変更不可のデータコレクションをどうやって変更するかというと、データのコピーを作ってコピーの方を変更する。一回の変更ごとにコピーを作ると普通に考えたら馬鹿馬鹿しいぐらいのリソースを消費してしまうが、変更されていない部分を共有するなどの工夫でコピーの作成はチープで済む。

OOではアイデンティティと値の区別を付けずに一緒にして考える。例えば、飛行機の座標クラスのインスタンス boeing787 は座標を表すフィールド x と y を持っている。座標を変更するにはboeing787インスタンスの x と y フィールドを変更する。これがアイデンティティと値が一緒になっているということである。
Clojure ではある飛行機の座標を表すアイデンティティ boeing787 に 値: 座標 [x1, y1] を結び付ける。Clojureで座標を変更するというのはアイデンティティ boeing787 に新しい値: 座標 [x2, y2] を結び付けるということである。値が変わるのではなく、値とアイデンティティの結び付きが変わるのだ。座標 [x1, y1] は未来永劫 [x1, y1] であり、[x2, y2] も永遠に変わることのない値である。
値と変化--Clojureにおける値のアイデンティティとステート

  • 強力な並行処理のツール

Atom, Ref, Agent など強力な並行処理のツールを用意している。それぞれの詳細はここでは省略。データの変更不可な性質のおかげで並行処理の厄介な問題の多くを回避できている。


;引数の分配束縛
(defn swap [[x y]]
   [y x])
(swap [1 2])
;=> [2 1]

; let の分配束縛
(let [[one two & res] [1 2 3 4]]
   [one res])
;=> [1 (3 4)]

関数の引数や let で値を受け取るときに、シーケンス中の特定の部分だけ直接受け取れる。代わりに多値を返せないのが少し残念だが、少しの記述で細かく直感的にデータをバインドでき、ネストもできるので強力。

  • mapやvectorが括弧の先頭にあると自己参照の関数として働く
([1 2 3] 0)
;=> 1
({:id 1 :name "clojure"} :id)
;=> 1

簡潔に書ける。このアイデア人気の言語を作るにはで言及されていた。

  • 例外処理を強制されない

Java のコードを呼び出す時に検査例外を捕まえたり投げたりを宣言しなくていい。Eclipse をなだめるだけのやっつけの catch なんて書かなくていい。もちろん例外を処理しようと思えばできる。

  • 文字列中の改行ができる
user> (println "abc
def
ghi")
               
abc
def
ghi
nil

Javaよりは少し良い。pythonのトリプルダブルクオーテーション程は良くない。

  • lispの慣習的な名前を取っ払った

car -> first
cdr -> rest
lambda -> fn

Paul Graham は car と cdr は並んだ時3文字で揃ってるのがいいとか言ってたような気がするが、不必要に初心者を混乱させてるのも事実。こだわる人は定義し直せば解決。fn は更に短く #(+ 1 %) とかで作れる。好き。

  • let と cond の括弧が少なくて綺麗
(let [x 1 y 2]
  (+ x y))
(cond (= x y) (func1)
      (> x y) (func2))

また関係ないが、 Arc では更に cond を無くして if だけにしている。確かにその方が綺麗だ。しかし、Arc の let を変数一個の時だけに使うというのは微妙だと思う。多くの let は一つの変数束縛しかしていないから一個固定のを用意して簡潔な記述にするというアイデアだが、Paul Graham が仰る探検的プログラミングでは追加も楽にできた方がいいのではないかと思うからだ。

scheme:

(define (func1 x)
  (+ x 1))

Clojure:

(defn func1 [x]
  (+ x 1))

名前と引数が分離されてた方が自分には見易い。defn という文字に def と fn が重なっててちょっと気持ちいい。
引数でのマッチングもできる。

(defn add2
  ([x] (+ x 2))
  ([x y] (+ x y 2)))
  • python の doctest に似た手軽なテストが書ける
(defn
  #^{:test (fn []
             (assert (= (add2 3) 5))
             (assert (= (add2 -4) -2))
             (assert (> (add2 50) 50)))}
  add2
  ([x] (+ x 2)))

user> (test add2)
:ok   

テストを小さく始める事ができる。関数の横にあれば使い方のexampleとして参照できる。
追記: 実際にはこの方法はまず使わなかった。

  • リスト内包表記

pythonHaskell のようにリスト内包表記が用意されている。自分には有効性が分からないのが、シーケンスを複数与えた時の以下の動作。

user> (for [x "ABC" y (range 3)] [x y])
([\A 0] [\A 1] [\A 2] [\B 0] [\B 1] [\B 2] [\C 0] [\C 1] [\C 2])

うーん、この動作はそんなに使うだろうか。([\A 0] [\B 1] [\C 2]) これが欲しかったら普通にmapを使おうということかな。

user> (map #(vector %1 %2) "ABC" (range 3))
([\A 0] [\B 1] [\C 2]) 
  • 末尾再帰の最適化について

普通の記述における末尾再帰の最適化は残念ながらされない。JVMの仕様らしい。しかし、どういうトリックなのか分からないが明示的に recur を使えばしてくれる。

(loop [result [] x 5]
  (if (zero? x)
    result
    (recur (conj result x) (dec x))))
[5 4 3 2 1]

プログラミングClojureより引用