Clojure勉強日記(その12 3.1 すべてはシーケンスである
こんにちは。連休が挟まったということで若干間があいていますが、続けます。
で、この間にプログラミングClojureの第2版が発売されました。
- 作者: Stuart Halloway and Aaron Bedra,川合史朗
- 出版社/メーカー: オーム社
- 発売日: 2013/04/26
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (10件) を見る
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で一般的な業務システムを開発するのはあまり向いておらず、
並行処理系のライブラリやフレームワークを作る方に適する・・・ということになるんでしょうか。