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

夢とガラクタの集積場

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

Clojure勉強日記(その13 3.2 シーケンスライブラリを使う

では、続きます。
前回のClojure記事にもあるように、環境がSublime Text2に移ったのでかなり快適になっています。

Clojureではシーケンスに対する関数が多くありますが、それらは下記のカテゴリに分類されるとのこと。

  1. シーケンスを生成する関数
  2. シーケンスをフィルタする関数
  3. シーケンスに対する述語
  4. シーケンスを変換する関数

・・・と言っても、そもそもシーケンスは変更できないため、
結局のところは作るか、フィルタするか、変換するか・・・というため述語はいまいちわかりません。
ともあれ、実際の関数を確認します。

1.シーケンスを生成する

まずはシーケンス生成関数を確認。数が多いため詳細は省略してコードとコメントで行きます。

  • range
; 1引数を指定すると0からその数までのシーケンスを作成
user=> (range 10)
(0 1 2 3 4 5 6 7 8 9)
; 作成されるシーケンスはLazySeq・・・おそらく、遅延初期化されるシーケンスということ?
user=> (class (range 10))
clojure.lang.LazySeq
; 2つ引数で開始値と終了値を定義可能
user=> (range 1 5)
(1 2 3 4)
; 3つ引数で各要素の間隔を定義可能
user=> (range 1 5 2)
(1 3)
  • repeat
; 繰り返し回数、要素を引数に指定
user=> (repeat 3 5)
(5 5 5)
; 生成されるクラスは同じくLazySeq
user=> (class (repeat 3 5))
clojure.lang.LazySeq
; 数値以外の要素も繰り返し生成可能
user=> (repeat 3 "TestTestTest")
("TestTestTest" "TestTestTest" "TestTestTest")
; 1引数で呼び出すと無限生成。REPLで入力すると死亡フラグ
user=> (repeat "XXX")
("XXX" "XXX" "XXX"・・・・
  • iterate
; 第二引数を初期値として現在の値に第一引数に関数を適用して無限ループ
user=> (iterate (fn [x] (+ 10 x)) 100)
(100 110 120・・・
; 途中で中断して取得するためにはtake関数が必要
(user=> (take 10 (iterate (fn [x] (+ x 10)) 100))
(100 110 120 130 140 150 160 170 180 190)
; repeatも同じくtake関数で絞り込むことが可能
user=> (take 10 (repeat "XXX"))
("XXX" "XXX" "XXX" "XXX" "XXX" "XXX" "XXX" "XXX" "XXX" "XXX")
; 要素がtakeで指定した数に満たない場合は全て表示される
user=> (take 10 (repeat 5 "XXX"))
("XXX" "XXX" "XXX" "XXX" "XXX")
; 生成される型はCons
user=> (class (iterate (fn [x] (+ x 10)) 100))
clojure.lang.Cons
  • cycle
; 取得したコレクションを延々ロードし続ける
user=> (take 14 (cycle (range 5)))
(0 1 2 3 4 0 1 2 3 4 0 1 2 3)
; 生成される型はLazySeq
user=> (class (cycle (range 5)))
clojure.lang.LazySeq
  • interleave
; 複数のコレクションの要素をマージしてどちらかがなくなるまで結合
user=> (interleave (repeat 10) (range 5))
(10 0 10 1 10 2 10 3 10 4)
; 生成される型はLazySeq
user=> (class (interleave (repeat 10) (range 5)))
clojure.lang.LazySeq
  • interleave
; 複数のコレクションの要素をマージしてどちらかがなくなるまで結合
user=> (interleave (repeat 10) (range 5))
(10 0 10 1 10 2 10 3 10 4)
; 生成される型はLazySeq
user=> (class (interleave (repeat 10) (range 5)))
clojure.lang.LazySeq
  • interpose
; コレクションの要素に中間要素を追加
user=> (interpose "?" (range 5))
(0 "?" 1 "?" 2 "?" 3 "?" 4)
; 生成される型はLazySeq
user=> (class (interpose "?" (range 5)))
clojure.lang.LazySeq
  • join
; コレクションの要素をマージして一要素にする
user=> (use '[clojure.string :only (join)])
nil
user=> (join (range 5))
"01234"
user=> (join " " (range 5))
"0 1 2 3 4"
  • コレクション名称の関数
; list
user=> (list 1 2 3)
(1 2 3)
; 作成されるのはPersistentList
user=> (class (list 1 2 3))
clojure.lang.PersistentList
; vector 
user=> (vector 1 2 3)
[1 2 3]
; 作成されるのはPersistentVector
user=> (class (vector 1 2 3))
clojure.lang.PersistentVector
; hash-set
user=> (hash-set 1 2 3)
#{1 2 3}
; 作成されるのはPersistentHashSet
user=> (class (hash-set 1 2 3))
clojure.lang.PersistentHashSet
; hash-map
user=> (hash-map 1 2 3 4)
{1 2, 3 4}
; 奇数だと要素がないと怒られる
user=> (hash-map 1 2 3)
IllegalArgumentException No value supplied for key: 3  clojure.lang.PersistentHashMap.create (PersistentHashMap.java:77)
; 作成されたハッシュマップから値取得可能
user=> ((hash-map 1 2 3 4) 3)
4
user=> ({1 2, 3 4} 3)
4
; REPL上で動かすと動作は同じだが、実際に使っているマップオブジェクトは異なる
user=> (class {1 2, 3 4})
clojure.lang.PersistentArrayMap
user=> (class (hash-map 1 2 3 4))
clojure.lang.PersistentHashMap

と、こういう形で書いてみましたが結構数がありますね。
後は「遅延初期化」という要素を使うことによって演算待ちの状態(?)でオブジェクト自体は生成可能のようです。
LazySeq・・となっているのがそれですね。
実際にシーケンスにアクセスした際に中身が生成されるという。(下記)

user=> (def repeat-loop (repeat "xxx"))
#'user/repeat-loop
; アクセスすると無限ループのため死亡フラグ
user=> repeat-loop
("xxx" "xxx" "xxx"・・・・

このあたり、「シーケンスを単なる入れ物」とだけ思っていると理解しにくい概念ですね・・・
後は、take関数を使うと絞れるということは、おそらく遅延初期化の際に何かしらの「制限」をかける要素を渡しているんでしょうけど・・・
ともあれ、その辺りの遅延初期化は使い方を誤ると問題が発生しそうです。

実際、下記のページだと興味深い事例がありました。

http://kurohuku.blogspot.jp/2013/02/clojurelazyseq.html
===
これは、take が返す値がLazySeqであるために起こります。
補助関数(call-with-large-csv)の外側でLazySeqの要素が必要(replで表示したい)になると、 その時になってから実際の読み込み処理が行われますが、このときにはもう(with-openによって)入力ストリームはクローズしています。
===

・・・なるほど。
必要になった際に取得しに行く、というわけですか。

2.シーケンスをフィルタする

次は実際のシーケンスから値を取得するフィルタです。

  • filter
user=> (take 10 (filter (fn [x] (= 0 (mod x 3))) (range 100)))
(0 3 6 9 12 15 18 21 24 27)
user=> (filter (fn [x] (= 0 (mod x 3))) (range 100))
(0 3 6 9 12 15 18 21 24 27 30 33 36 39 42 45 48 51 54 57 60 63 66 69 72 75 78 81 84 87 90 93 96 99)
; 返されるのはやはり遅延シーケンス
user=> (class (filter (fn [x] (= 0 (mod x 3))) (range 100)))
clojure.lang.LazySeq
user=> (doc complement)
-------------------------
clojure.core/complement
([f])
  Takes a fn f and returns a fn that takes the same arguments as f,
  has the same effects, if any, and returns the opposite truth value.
nil
; 条件を満たすまで先頭から要素を取得するtake-while。complementはdocの通り、逆の判定を返す
user=> (take-while (complement #{\-}) "the-quick-brown-fox")
(\t \h \e)
; 返されるのはやはり遅延シーケンス
user=> (class (take-while (complement #{\-}) "the-quick-brown-fox"))
clojure.lang.LazySeq
; 条件を満たすまで値を捨て、それ以後の値を取得するdrop-while
user=> (drop-while (complement #{\-}) "the-quick-brown-fox")
(\- \q \u \i \c \k \- \b \r \o \w \n \- \f \o \x)
; インデックスを取り、コレクションを分割するsplit-at
user=> (split-at 10 "the-quick-brown-fox")
[(\t \h \e \- \q \u \i \c \k \-) (\b \r \o \w \n \- \f \o \x)]
user=> (class (split-at 10 "the-quick-brown-fox"))
clojure.lang.PersistentVector
user=> ((split-at 10 "the-quick-brown-fox") 0)
(\t \h \e \- \q \u \i \c \k \-)
user=> ((split-at 10 "the-quick-brown-fox") 1)
(\b \r \o \w \n \- \f \o \x)
user=> (class ((split-at 10 "the-quick-brown-fox") 1))
clojure.lang.LazySeq
; 関数を取り、コレクションを分割するsplit-with。但し、分割数は2限定の模様。
user=> (split-with (complement #{\-}) "the-quick-brown-fox")
[(\t \h \e) (\- \q \u \i \c \k \- \b \r \o \w \n \- \f \o \x)]
user=> (doc split-with)
-------------------------
clojure.core/split-with
([pred coll])
  Returns a vector of [(take-while pred coll) (drop-while pred coll)]
nil

3.シーケンスに対する述語

フィルタ関数は「述語」を取ってシーケンスを返しています。
但し、これはあくまでシーケンスの内部要素に対する述語となっています。
ここでいう「シーケンスに対する述語」とは、「他の述語がシーケンスの各要素に対してどのような結果を返すかを調べる」というものだそうです。
例えば、every?(全て真か?)や、some(どれか真か?)だとか。

; 全て述語の条件を満たしていればtrueとなる。
user=> (every? (complement #{\-}) "the")
true
user=> (every? (complement #{\-}) "the-")
false
; someは述語が偽でない値を取った場合に結果を返す。そのため、truefalseでは返らないケースもある。
; この場合、#{\-}返り値が「文字列」のため結果も「文字列」
user=> (some #{\-} "the-")
\-
user=> (some #{\-} "the")
nil
; この場合、even?の返り値がbooleanのため、返り値もtrue
user=> (some even? [1 2 3])

他の述語はこんな感じ。

user=> (not-any? (complement #{\-}) "the-")
false
user=> (not-any? (complement #{\-}) "the")
false
user=> (not-every? (complement #{\-}) "the")
false
user=> (not-every? (complement #{\-}) "the-")
true
user=> (not-any? (complement #{\-}) "---")
true

但し、このあたりの全て中身見て実施する系は無限のコレクションに対して実行するとひどいことになるケースもありますね。

4.シーケンスの変換

シーケンスの変換、は各要素の値を変換する関数になります。
例えば、下記のmap関数は各collの要素に対して関数fを適用するなど。
(map f coll)
尚、collは複数指定することができ、その場合はcollの数だけ引数を取る関数fを指定し、
いずれかの要素がなくなるまで実施するということが可能だそうな。

user=> (map (fn [target] (format "<p>%s</p>" target)) ["the" "format" "test"])
("<p>the</p>" "<p>format</p>" "<p>test</p>")
user=> (map (fn [target1 target2 target3] (format "%s-%s-%s" target1 target2 target3)) ["one" "two" "three"] ["1" "2" "3" "4"] ["ONE" "TWO"])
("one-1-ONE" "two-2-TWO")

確かに。これはこれで少ない記述で書けますねぇ・・・
後は下記のreduce関数は「fをまずcollの最初の要素と2番目の要素に適用し、結果と3番目の要素に適用し・・」となるとのこと。
その他もsortやsortbyといった関数があります。

user=> (map (fn [target] (format "<p>%s</p>" target)) ["the" "format" "test"])
("<p>the</p>" "<p>format</p>" "<p>test</p>")
user=> (map (fn [target1 target2 target3] (format "%s-%s-%s" target1 target2 target3)) ["one" "two" "three"] ["1" "2" "3" "4"] ["ONE" "TWO"])
("one-1-ONE" "two-2-TWO")

このあたりの概念を内包するのが「リスト内包表記」であるとのこと。
forマクロを使って下記のように記述される。
(for [binding-form coll-expr filter-expr? ...] expr)
for はbinding-form、coll-expr、そして省略可能なfilter-expr を並べた
ベクタを取り、expr の結果を要素とするシーケンスを返す・・とのことです。
さっぱりわかりまへん(汗

実際に動かしている例を確認してみます。

; filter-exprを省略したパターン
user=> (for [targetint [1 2 3 4]] (format "-%s-" targetint))
("-1-" "-2-" "-3-" "-4-")
; filter-exprを入れてみますが・・・あれ?
user=> (for [targetint [1 2 3 4] (fn [i] (= (% i 2) 0))] (format "-%s-" targetint))
IllegalArgumentException for requires an even number of forms in binding vector in user:31  clojure.core/for (core.clj:4190)
; binding vectorが偶数のみと。どうやらこう書く模様。
user=> (for [targetint [1 2 3 4] :when (= (mod targetint 2) 0)] (format "-%s-" targetint))

尚、これを複数のbinding-form coll-exprのペアを用意すると下記のようになる。

user=> (for [targetint [1 2 3 4] targetstr ["A" "B"]] (format "-%s-%s-" targetint targetstr))
("-1-A-" "-1-B-" "-2-A-" "-2-B-" "-3-A-" "-3-B-" "-4-A-" "-4-B-")
user=> (for [targetstr ["A" "B"] targetint [1 2 3 4]] (format "-%s-%s-" targetint targetstr))
("-1-A-" "-2-A-" "-3-A-" "-4-A-" "-1-B-" "-2-B-" "-3-B-" "-4-B-")
user=> (for [targetstr ["A" "B"] targetint [1 2 3 4] targetmark ["*" "#" "&"]] (format "-%s-%s-%s-" targetstr targetint targetmark))
("-A-1-*-" "-A-1-#-" "-A-1-&-" "-A-2-*-" "-A-2-#-" "-A-2-&-" "-A-3-*-" "-A-3-#-" "-A-3-&-" "-A-4-*-" "-A-4-#-" "-A-4-&-" "-B-1-*-" "-B-1-#-" "-B-1-&-" "-B-2-*-" "-B-2-#-" "-B-2-&-" "-B-3-*-" "-B-3-#-" "-B-3-&-" "-B-4-*-" "-B-4-#-" "-B-4-&-")

右側の要素をなめて、そのあと一つ左の要素をなめて、それが終わればさらに左・・・・と、
後に指定した要素からの多重ループが簡単に作れてしまうわけですか。

今回結果は副作用のない単なる結果を返す関数ですが、
副作用を持たせて繰り返し処理・・・というのは簡単にできそうですね。