読者です 読者をやめる 読者になる 読者になる

夢とガラクタの集積場

落ちこぼれ三流エンジニアである管理人の夢想=『夢』と、潰えた夢=『ガラクタ』の集積場です。

Clojure勉強日記(その12 3.1 すべてはシーケンスである

こんにちは。連休が挟まったということで若干間があいていますが、続けます。

で、この間にプログラミングClojureの第2版が発売されました。

プログラミングClojure 第2版

プログラミングClojure 第2版

なので、途中まで読んだ第1版はとりあえず置いておいて、第2版の方に入って読んでいきます。
2章までの構成は同じなので、そこまではいいとして、
3章が「シーケンスを使ったデータの統合」のため、まずはそこから。

Clojureに限らず、プログラムはデータを扱っています。
一番具体的なものとしては文字列、リストといった「具体的な構造」となります。
だが、下記のようにプログラムで扱う少なくないものが「データ構造」という意味で同一として扱うことができるとのこと。

  • XMLデータはツリー構造
  • データベースへの問い合わせ結果はリスト
  • ディレクトリ階層はツリー

Clojureではこれらすべてのデータ構造を単一の抽象概念=シーケンスとして扱うようです。
確かに抽象化すれば同じです。

シーケンスを扱うライブラリこそClojureにとってのコレクションAPIにあたるもののようですね。

というわけで(?)、まずはClojureシーケンスのコアとなるAPIから確認していきます。

; first シーケンスの最初の要素取得(ない場合はnil)。取得される型は要素の型
user=> (first '(1))
1
user=> (class (first '(1)))
java.lang.Long
user=> (first ())
nil
; rest シーケンスの2個目以降の要素を取得(要素が1つ、または空の場合は空シーケンス)。取得される型はPersistentList
user=> (rest ())
()
user=> (class (rest '()))
clojure.lang.PersistentList$EmptyList
user=> (rest '(5))
()
user=> (class (rest '(5)))
clojure.lang.PersistentList$EmptyList
user=> (rest '(5 6 7))
(6 7)
user=> (class (rest '(5 6 7)))
clojure.lang.PersistentList
; cons 既存のシーケンスの頭に要素を加えたシーケンスを作成。取得される型はCons
user=> (cons 1 '(5 6 7))
(1 5 6 7)
user=> (class (cons 1 '(5 6 7)))
clojure.lang.Cons
user=> (cons '(1 2 3) '(5 6 7))
((1 2 3) 5 6 7)
user=> (class (cons '(1 2 3) '(5 6 7)))
clojure.lang.Cons

実装レベルではこれらの機能はJavaインタフェースとしてISeqクラスで宣言されている・・・
とありますが、下記のページを見てみると実はrestメソッドはないという罠。
後は重要なポイントとして「シーケンスはイミュータブル」と記述されていました。
cons等は別のシーケンスを生成して返すようですね。
http://grepcode.com/file/repo1.maven.org/maven2/org.clojure/clojure/1.5.0/clojure/lang/ISeq.java
次はseq関数とnext関数。
seqはシーカブル(イテレートでおえる、という感じ?)を作成する関数とのこと。

; 単一オブジェクトからは生成できない
user=> (seq 1)
IllegalArgumentException Don't know how to create ISeq from: java.lang.Long  clojure.lang.RT.seqFrom (RT.java:505)
; リストから生成。というか'の時点でシーケンスであるが
user=> (seq '(1))
(1)
; Class確認。PersistentListクラスとなっている。
user=> (class (seq '(1)))
clojure.lang.PersistentList
user=> (class (seq '(1 2 3)))
clojure.lang.PersistentList
user=> (seq '(1 2 3))
(1 2 3)
user=> (seq (cons 1 '(1 2 3)))
(1 1 2 3)
user=> (seq (cons '(1 2 3) '(1 2 3)))
((1 2 3) 1 2 3)
; Consを用いてシーケンスを生成した場合、PersistentListではなくCons
user=> (class (seq (cons '(1 2 3) '(1 2 3))))
clojure.lang.Cons
user=> (class (cons '(1 2 3) '(1 2 3)))
clojure.lang.Cons
; nextの確認
user=> (next '(1 2 3))
(2 3)
user=> (next '(1))
nil
user=> (class (next '(1 2 3)))
clojure.lang.PersistentList

下記の2ソースを確認したところ、共通の親はASeqでした。
そのため、シーケンス=ASeqということなんでしょう。
https://code.google.com/p/clojure/source/browse/trunk/src/jvm/clojure/lang/PersistentList.java
https://code.google.com/p/clojure/source/browse/trunk/src/jvm/clojure/lang/Cons.java
このあたり、他とは違うクラスとなっているあたり、さすが昔からあるconsです。

Clojureではリストだけでなくベクタをシーケンス化することもできるそうです。

user=> (first [1 2 3])
1
; ベクタのシーケンス。PersistentListではなくPersistentVectorのクラスとして表現される模様。
user=> (class (seq [1 2 3]))
clojure.lang.PersistentVector$ChunkedSeq
user=> (class (rest [1 2 3]))
clojure.lang.PersistentVector$ChunkedSeq
user=> (class (cons 5 [1 2 3]))
clojure.lang.Cons
; consは変わらずCons
user=> (class (cons [4 5] [1 2 3]))
clojure.lang.Cons
user=> (cons [4 5] [1 2 3])
([4 5] 1 2 3)

seqやconsはリストやベクタだけでなく、マップやセットにも適用可能。
とまぁ、確かにEntryのリストのようなものですからね。

user=> (first {:key "key" :value "value"})
[:key "key"]
; firstだとMapEntryが返る
user=> (class (first {:key "key" :value "value"}))
clojure.lang.MapEntry
user=> (class (first {:key "key" :value "value" :message "message"}))
clojure.lang.MapEntry
user=> (rest {:key "key" :value "value" :message "message"})
([:message "message"] [:value "value"])
; restだとPersistentArrayMap$Seqが返る
user=> (class (rest {:key "key" :value "value" :message "message"}))
clojure.lang.PersistentArrayMap$Seq
; consはMapEntryかベクタ辺りののシーケンスが返る模様。型は相変わらずCons
user=> (cons {:key "key" :value "value"} {:message "message"})
({:key "key", :value "value"} [:message "message"])
user=> (cons {:key "key" :value "value"} {:value "test" :message "message"})
({:key "key", :value "value"} [:message "message"] [:value "test"])
user=> (cons [:key "key"] {:value "test" :message "message"})
([:key "key"] [:message "message"] [:value "test"])
user=> (class (cons [:key "key"] {:value "test" :message "message"}))
clojure.lang.Cons

同様にセットも扱える。

user=> (first #{:key :value :message})
:key
user=> (rest #{:key :value :message})
(:message :value)
; Setの場合はAPersistentMap$KeySeqが使用される模様
user=> (class (rest #{:key :value :message}))
clojure.lang.APersistentMap$KeySeq
user=> (cons :test #{:key :value :message})
(:test :key :message :value)
user=> (cons #{:test :label} #{:key :value :message})
(#{:test :label} :key :message :value)

こう見ていると、気になってくるのがMapとKeyのキーワードの並び順ですね。
それを一定化するためにClojureではおなじみのsortedシリーズが存在するようです。

user=> (sorted-set :key :value :message :test)
#{:key :message :test :value}
; PersistentTreeSetという形で作られる模様
user=> (class (sorted-set :key :value :message :test))
clojure.lang.PersistentTreeSet
user=> (sorted-map :key 5 :value 2 :message 9 :test 1)
{:key 5, :message 9, :test 1, :value 2}
; PersistentTreeMapという形で作られる模様
user=> (class (sorted-map :key 5 :value 2 :message 9 :test 1))
clojure.lang.PersistentTreeMap

後はconj関数(1つ以上の要素を追加)と、into(2つのコレクションをあわせる)
尚、どちらの関数も与えられたコレクションに効率的にデータを渡すことのできる手段を取る。
例えばリストでは先頭、ベクタの場合末尾など。

user=> (conj '(1 2 3) :a :b)
(:b :a 1 2 3)
user=> (class (conj '(1 2 3) :a :b))
clojure.lang.PersistentList
user=> (conj [1 2 3] :a :b)
[1 2 3 :a :b]
; conjの場合Consではなく元々のものが用いられる模様
user=> (class (conj [1 2 3] :a :b))
clojure.lang.PersistentVector
user=> (into '(1 2 3) '(:key :value))
(:value :key 1 2 3)
user=> (class (into '(1 2 3) '(:key :value)))
clojure.lang.PersistentList
user=> (into [1 2 3] [:key :message])
[1 2 3 :key :message]
user=> (class (into [1 2 3] [:key :message]))
clojure.lang.PersistentVector

その他の情報として、ほとんどのClojureのシーケンスは遅延評価されるため
巨大なシーケンスを扱う際にも容量を節約したまま用いることが可能となっているようです。

後は重要なポイントして、「Clojureのシーケンスは変更不可」ということがあります。
上記でconjで追加している・・・というように見えますが、
実際は別のインスタンスに中身をコピーし、追加したものを返している・・・というわけですね。

これは業務ロジック等で作成したリストを使いまわしたい場合などはかなり使いにくい性質ですが、
並列処理が走る状況下においては誤ったアクセスにおいてデータ構造が破壊されることがないというのは大きな利点となります。

オブジェクトを作る際のコストについて目が行きますが、
その辺りは当然Clojure(というか関数型言語全般)で当然対応されているでしょうから、
先入観で毎回作るのは遅い、と考えてしまうのは古いJavaエンジニアの宿命なんでしょう。きっと。

そのため、Clojureで一般的な業務システムを開発するのはあまり向いておらず、
並行処理系のライブラリやフレームワークを作る方に適する・・・ということになるんでしょうか。