previous up next
Previous: 1 動的開発 Up: 特徴 (翻訳開始前) Next: 3 Lisp方言としてのClojure

Subsections


2 関数型プログラミング

Clojureは関数型プログラミング言語だ。変更可能なステートを避けるツールがあり、関数は第一級オブジェクト、副作用を利用したループではなく再帰による繰り返しを推奨している。だが、参照透過性を強制しない、「証明可能」なプログラムを目指さない、などといった点でClojureは純粋関数型言語ではない。Clojureの哲学は、プログラムのほとんどの部分は関数的であるべきで、関数的な部分が多いほどプログラムは堅牢になる、というものだ。

2.1 第一級関数 (ファーストクラスの関数)

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で作られたローカル名は変数ではない。つまり値は絶対に変化しない!

2.2 イミュータブルなデータ構造

ステートが変化するのを避ける最も良い方法は、イミュータブル(変更不可能)なデータ構造を用いることだ。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)
    ;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))
Applications often need to associate attributes and other data about data that is orthogonal to the logical value of the data. Clojure provides direct support for this metadata. Symbols, and all of the collections, support a metadata map. It can be accessed with the meta function (reader macro ). Metadata does not impact equality semantics, nor will metadata be seen in operations on the value of a collection. Metadata can be read, and can be printed.
(def v [1 2 3])
(def attributed-v (with-meta v {:source :trusted}))
(:source ^attributed-v)
-> :trusted
(= v attributed-v)
-> true
Extensible Abstractions Clojure uses Java interfaces to define its core data structures. This allows for extensions of Clojure to new concrete implementations of these interfaces, and the library functions will work with these extensions. This is a big improvement vs. hardwiring a language to the concrete implementations of its data types.

A good example of this is the seq interface. By making the core Lisp list construct into an abstraction, a wealth of library functions are extended to any data structure that can provide a sequential interface to its contents. All of the Clojure data structures can provide seqs. Seqs can be used like iterators or generators in other languages, with the significant advantage that seqs are immutable and persistent. Seqs are extremely simple, providing a first function, which return the first item in the sequence, and a rest function which returns the rest of the sequence, which is itself either a seq or 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)]
Many of the Clojure library functions produce and consume seqs lazily:
;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)
You can define your own lazy seq-producing functions using the lazy-cons macro, which takes the first item in the sequence and an expression which will be called on demand to produce the rest of the sequence. Here's a simplified take:
(defn take [n coll]
  (when (and (pos? n) (seq coll))
    (lazy-cons (first coll) (take (dec n) (rest coll)))))
Recursive Looping In the absence of mutable local variables, looping and iteration must take a different form than in languages with built-in for or while constructs that are controlled by changing state. In functional languages looping and iteration are replaced/implemented via recursive function calls. Many such languages guarantee that function calls made in tail position do not consume stack space, and thus recursive loops utilize constant space. Since Clojure uses the Java calling conventions, it cannot, and does not, make the same tail call optimization guarantees. Instead, it provides the recur special operator, which does constant-space recursive looping by rebinding and jumping to the nearest enclosing loop or function frame. While not as general as tail-call-optimization, it allows most of the same elegant constructs, and offers the advantage of checking that calls to recur can only happen in a tail position.
(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))
             (rest my-keys)
             (rest my-vals))
      my-map)))
(my-zipmap [:a :b :c] [1 2 3])
-> {:b 2, :c 3, :a 1}
For situations where mutual recursion is called for, recur can't be used. Instead, trampoline may be a good option.


previous up next
Previous: 1 動的開発 Up: 特徴 (翻訳開始前) Next: 3 Lisp方言としてのClojure
MARUI Atsushi
2013-01-12