2011年4月23日土曜日

「プログラミングclojure」を読んでみた。その1--apply関数編--

0 コメント

「プログラミングclojure」を読んでいます。とりあえず流し読みが終わって今2巡目。

その中でsnake.cljといういスネークゲームのプログラムがあるんだけどその中で定義されているadd-pointsの中身が理解できない。


その関数の中身。

(defn add-points [& pts]
(vec (apply map + pts)))

これは座標を更新する関数。最初この関数の中身がわからなかった。

add-pointsは第一引数に現在の座標、第二引数に移動量を表すベクタを取る。

動作は以下。

user> (add-points [10 10] [1 -1])
[11 9]

よくわからないのはこの1行。apply関数。

(vec (apply map + pts)))

map はコレクションの各要素に第一引数の関数を適用して

それぞれの返り値をひとつのシーケンスにして返す。

user> (map + [10 10] [1 1])
(11 11)

返り値をベクタにする必要があるからvecを使うのはわかる。

user> (vec (map + [10 10] [1 1]))
[11 11]

理解を妨げているのはapply関数。

なんで必要なの?これじゃダメ?

(defn wrong-add-points [& pts]
(vec (map + pts)))

しかし実際に試すど動作しない。

user> (wrong-add-points [10 10] [1 -1])
java.lang.ClassCastException
[Thrown class java.lang.RuntimeException]

apply関数を調べてみる。

user> (doc apply)
-------------------------
clojure.core/apply
([f args* argseq])
Applies fn f to the argument list formed by prepending args to argseq.
nil

APIマニュアルを読んだけどよくわからんww。

でも動かしてみたらなんとなく動作理解。

user> (apply str '(1 2 3))
"123"
user> (apply str \A '(\B \C))
"ABC"

要はstr以降の引数のカッコをとって適用する感じだね。





で、add-pointsの中での使い方をもう一度見てみる。

(defn add-points [& pts]
(vec (apply map + pts)))

ptsは可変引数だから関数内部ではリストになっている。

ここで、可変長引数の動作確認のために関数を定義しみた。

user> (defn hoge-points [& pts] (println pts))
#'user/hoge-points
user> (hoge-points [1 1] [1 1])
([1 1] [1 1])
nil

やはりptsがリストになるのね。

add-pointsの引数ptsも同様にリストで受け取るわけだ。

だからこのptsをmapにそのままわたしてもうまくいかない。

先に作ったapplyを使わないadd-pointsだとmapにリストが渡されてしまう。

user> (wrong-add-points [10 10] [1 1])

この時のmapは次のように実引数が渡されることになる。

(map + ([10 10] [1 1])

試したら同じエラーが出た。

user> (map + '([10 10] [1 1]))
java.lang.ClassCastException
[Thrown class java.lang.RuntimeException]

期待する動作としてはカッコを外して渡したいと。

user> (map + [10 10] [1 1])
(11 11)

ここでやっとapplyを使う理由がわかった。

applyを使ってmapを使えばptsのカッコが外れてmapが適用できる。

user> (apply map + '([10 10] [1 1]))
(11 11)

返り値はベクタでほしいのでvecを噛まして完成と。

user> (vec (apply map + '([10 10] [1 1])))
[11 11]

ちなみにmapの説明を忘れていた。

map

以下mapのドキュメント。

user> (doc map)
-------------------------
clojure.core/map
([f coll] [f c1 c2] [f c1 c2 c3] [f c1 c2 c3 & colls])
Returns a lazy sequence consisting of the result of applying f to the
set of first items of each coll, followed by applying f to the set
of second items in each coll, until any one of the colls is
exhausted.  Any remaining items in other colls are ignored. Function
f should accept number-of-colls arguments.
nil

う、思ったよりも長いヘルプ。

(map f coll)

とあった場合、collはシーケンスでcollのそれぞれの要素fを適用して

最後にシーケンスにして返す。

user> (map #(format "<p>%s</p>" %) [ "the" "quick" "brwon" "fox" ])
("<p>the</p>" "<p>quick</p>" "<p>brwon</p>" "<p>fox</p>")

複数のコレクションを取る場合、fはそのコレクションと同じ引数を取る関数でなければならない。

適用される要素の数は、一番数の少ないコレクションの数に合わされる。

user> (map #(format "<%s>%s</%s>" %1 %2 %1) [ "h1" "h2" "h3" ] [ "the" "quick" "brwon" "fox" ])
("<h1>the</h1>" "<h2>quick</h2>" "<h3>brwon</h3>")

で、add-pointsの場合、ptsが複数のコレクション(ベクタ)をとる。

user> (map + [10 9] [1 -1])
(11 8)

この時

(list (+ 10 1) (+ 9 -1))

のように動作する。

所感

自前の関数で受け取った引数をまるごとmapに渡すことってなんとなく結構ありそうな気がするから、add-pointsのようなパターンって結構あるのかなと思う。
add-pointsは中身なんてたった1行の関数なのに理解まで結構時間が掛かった。
解ってしまえばなんということのない動作だけども、ここにたどり着くまでにいろいろ寄り道が必要になる。
新しいことを学ぶときは最初はどうしても疑問が多いから時間がかるよね。
自分の知識が増えれば、わからないピースが埋まった状態で考えることができるから素早くタイプ出来るんだろうけど。
Lisperへの道は長い。