レガシーコード生産ガイド

私に教えられることなら

少しだけClojure 3

プログラミングClojure第2版を読みながら続き。

あっちこっち行ったり来たりしている。

マップのmerge-withの動作

concatの例しか無いけど、2引数関数を取って、先頭のマップから見ていき、同じ値のキーが出てきたときに比較してるのかな?

user=> (merge-with max {:a 1 :b 2} {:a 5 :c 3})
{:c 3, :b 2, :a 5}

user=> (merge-with max {:a 1 :b 2} {:a 5 :c 3} {:a 6 :c 4})
{:c 4, :b 2, :a 6}

user=> (merge-with (fn [l r] l) {:a 1 :b 2} {:a 5 :c 3} {:a 6 :c 4})
{:c 3, :b 2, :a 1}

user=> (merge-with (fn [l r] r) {:a 1 :b 2} {:a 5 :c 3} {:a 6 :c 4})
{:c 4, :b 2, :a 6}

そうっぽい。実際に値を表示してみてたしかめる。

user=> (merge-with (fn [l r] (println l ":" r) r) {:a 1 :b 2} {:a 5 :c 3} {:a 6 :c 4})
1 : 5
3 : 4
5 : 6
{:c 4, :b 2, :a 6}

合ってるみたいだ。

シーケンス化したマップをマップに戻す

といえばいいのかな、マップにrestを適用した結果をマップに戻す。

user=> (rest {:a 1 :b 2 :c 3 :d 4 :e 5})
([:c 3] [:b 2] [:d 4] [:a 1])

user=> (reduce (fn [acc [k v]] (assoc acc k v)) {} *1)
{:a 1, :d 4, :b 2, :c 3}

assocとreduceの動作、Common Lispと混ざりそうだ。

無名関数の略記法も使ってみる。分配は無理かな?

user=> (reduce #(assoc %1 (first %2) (second %2)) {} (rest {:a 1 :b 2 :c 3 :d 4 :e 5}))
{:a 1, :d 4, :b 2, :c 3}

シーケンスのフィルタ

集合をそのまま関数として使えるのはそうとうアツい。

user=> (take-while #{ \a \b \c } "abcabcabcacbacacb love is over")
(\a \b \c \a \b \c \a \b \c \a \c \b \a \c \a \c \b)

seq/set使いこなせればいろいろ幅が広がりそうだ。

user=> (take-while (set "CountryGirls") "CountryGirls vs Mechagodzilla")
(\C \o \u \n \t \r \y \G \i \r \l \s)

user=> (clojure.string/join (take-while (set "CountryGirls") "CountryGirls vs Mechagodzilla"))
"CountryGirls"

split-withは最初に述語が偽になったとこで切り分けるみたいだ。

user=> (split-with #(> % 5) '(5 6 7 8 7 6 5 4 3 4 5 6 7 8 7 6 5 4 3))
[() (5 6 7 8 7 6 5 4 3 4 5 6 7 8 7 6 5 4 3)]

user=> (split-with #(> % 5) '(6 7 8 7 6 5 4 3 4 5 6 7 8 7 6 5 4 3))
[(6 7 8 7 6) (5 4 3 4 5 6 7 8 7 6 5 4 3)]

再度真になったところで切られたりはしない。

シーケンスの変換

reduceの変数の順番と初期値の位置をメモっておく

user=> (reduce #(cons %2 %1) '() '(a b c))
(c b a)

user=> (reduce (fn [acc x] (cons x acc)) '() '(a b c))
(c b a)

user=> (reduce + [1 2 3])
6

user=> (reduce + 5 [1 2 3])
11

左からで、[acc x]と渡されていく。reduceの引数、Haskellのfoldlとfoldrで混乱したままなので困る。いつか決着をつけねばなるまい。

ところでreduceは遅延シーケンスはどう扱うんだろう?多分reduceした時点で現実化(?)すると思うんだけど……

user=> (take 10 (reduce (fn [acc x] (cons x acc)) '() (cycle '(a b c))))

; 停止

帰ってこなかった。あってるっぽい。

Haskellのfoldlには再帰でスタック使い果たし問題があったけど、clojureのreduceはどうなんだろう?

user=> (reduce (fn [acc x] (cons x acc)) '() (take 100000 (cycle '(a b c))))

無事10万個のabcabc...が表示された。意外に速かった。多分再帰じゃなくてループということでしょう。

安心してガンガン使おう。

sort-byは関数を適用した結果を比べて「適用前の値」を並べ替えてくれる。便利だ。

user=> (sort > (take 20 (iterate dec 20)))
(20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1)

user=> (sort-by #(- 0 %) > (take 20 (iterate dec 20)))
(1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20)

遅延評価メモ

(take 20 (iterate dec 20))て毎回書くの面倒だからまとめるか、と考えててふと気づいたんだけど、遅延評価の利点の一つに引数による脳のスタックの消費を軽減できることがあるのかな。

例えばあるベクタのそれぞれの要素に、0から始まるをidとして付加する場合、正格評価ならインクリメントされる値の存在とその動作を意識しておかないといけないけど、遅延評価の場合は元のベクタと無限の数列を合わせていくだけなので、一つ意識する対象が減る、ってことかなと思った。

ただ実際の(特に自分が作ってるアプリのような)開発でどこまでこの利点が活かされるかは謎。あと「単に別の複雑さに置き換わっただけ」という可能性もある。

内包表記

遊んでたら止まった。

user=> (take 10 (for [n (whole-numbers) :when (< n 10)] n))

1から9までの数字を取得したあと、10以上でかつ10未満の数字を探そうと無限に数列を見ていったってことかな。試してみる。

user=> (take 10 (for [n (whole-numbers) :when (do (print n) (< n 10))] n))

無事大量の数字が表示された。気をつけよう。

内包表記は遅延シーケンスを返すらしい。試してみる。

user=> (for [rank (range 1 9) file "ABCDEFGH"] (do (print 'hey) (format "%c%d" file rank)))
("A1" "B1" "C1" "D1" "E1" "F1" "G1" "H1" "A2" "B2" "C2" "D2" "E2" "F2" "G2" "H2" "A3" "B3"
 "C3" "D3" "E3" "F3" "G3" "H3" "A4" "B4" "C4" "D4" "E4" "F4" "G4" "H4" "A5" "B5" "C5" "D5"
 "E5" "F5" "G5" "H5" "A6" "B6" "C6" "D6" "E6" "F6" "G6" "H6" "A7" "B7" "C7" "D7" "E7" "F7"
 "G7" "H7" "A8" "B8" "C8" "D8" "E8" "F8" "G8"
 "H8")heyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyhe
yheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyh
eyheyheyheyheyheyhey

やかましい。

遅延シーケンスが返されるなら、take 1で一つだけ取った場合、heyは一度しか表示されないはず。

user=> (take 1 (for [rank (range 1 9) file "ABCDEFGH"] (do (print 'hey) (format "%c%d" file rank))))
("A1")hey

遅延シーケンスだった。

ただ気になるのが、heyが表示される場所。なんで先に現実化された値("A1")の方が表示されてるんだろう?謎だ。

ついでに今の式をdefしてみると、メモ化されてるのもわかった。

user=> (def board (for [rank (range 1 9) file "ABCDEFGH"] (do (print 'hey) (format "%c%d" file rank))))
#'user/board

user=> board
("A1" "B1" "C1" "D1" "E1" "F1" "G1" "H1" "A2" "B2" "C2" "D2" "E2" "F2" "G2" "H2" "A3" "B3"
 "C3" "D3" "E3" "F3" "G3" "H3" "A4" "B4" "C4" "D4" "E4" "F4" "G4" "H4" "A5" "B5" "C5" "D5"
 "E5" "F5" "G5" "H5" "A6" "B6" "C6" "D6" "E6" "F6" "G6" "H6" "A7" "B7" "C7" "D7" "E7" "F7"
 "G7" "H7" "A8" "B8" "C8" "D8" "E8" "F8" "G8"
 "H8")heyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyhe
yheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyh
eyheyheyheyheyheyhey

user=> (doall board)
("A1" "B1" "C1" "D1" "E1" "F1" "G1" "H1" "A2" "B2" "C2" "D2" "E2" "F2" "G2" "H2" "A3" "B3"
 "C3" "D3" "E3" "F3" "G3" "H3" "A4" "B4" "C4" "D4" "E4" "F4" "G4" "H4" "A5" "B5" "C5" "D5"
 "E5" "F5" "G5" "H5" "A6" "B6" "C6" "D6" "E6" "F6" "G6" "H6" "A7" "B7" "C7" "D7" "E7" "F7"
 "G7" "H7" "A8" "B8" "C8" "D8" "E8" "F8" "G8" "H8")

一度しか評価されていない。

dorunの様子も見れる。

user=> (def board (for [rank (range 1 9) file "ABCDEFGH"] (do (print 'hey) (format "%c%d" file rank))))
#'user/board

user=> (dorun board)
heyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyhe
yheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyh
eyheyheyheyheynil
広告を非表示にする