F#のSeqに驚かされた。.NETのZipArchiveクラスのEntriesプロパティはReadOnlyCollection<ZipArchiveEntry>という型なのだが、こういった.NETでよく使われるコレクションはF#ではListの関数では直接扱えない。ところがSeqの関数では扱える。で、mapやiterなどSeqの関数をListの関数のつもりで(大概同じものがある)使ってるとその仕様の違いに突然驚かされる。で、確認のためプログラムを書いた。
let OnList (d0) = let _ = stderr.WriteLine("## OnList ##") let _ = stderr.WriteLine("part 1") let c = ref 0 let d1 = List.map (fun (s: string) -> let _ = stderr.WriteLine("function list 1") c := !c + 1 s.ToUpper() + string !c ) d0 if List.length d1 > 2 then let _ = stdout.WriteLine("part 2") let d2 = List.map (fun (s: string) -> let _ = stderr.WriteLine("function list 2") s ) d1 d2 else [] let OnSeq (d0) = let _ = stderr.WriteLine("## OnSeq ##") let _ = stderr.WriteLine("sect 1") let c = ref 0 let d1 = Seq.map (fun (s: string) -> let _ = stderr.WriteLine("function seq 1") c := !c + 1 s.ToUpper() + string !c ) d0 if Seq.length d1 > 2 then //if true then let _ = stderr.WriteLine("sect 2") let d2 = Seq.map (fun (s: string) -> stderr.WriteLine("function seq 2") s ) d1 Seq.toList d2 else [] [<EntryPoint>] let main argv = let ls1 = [ "abc"; "def"; "ghi" ] let r1 = OnList(ls1) let _ = List.iter (fun (s: string) -> stdout.WriteLine(s)) r1 let r2 = OnSeq(ls1) let _ = List.iter (fun (s: string) -> stdout.WriteLine(s)) r2 0
OnList関数とOnSeq関数はどちらも入力の文字列リストを関数1で大文字に変換して順序数を付けて、リストサイズが2より大きかったら関数2を通って変換後のリストを返す。関数2では何もさせてないが、何かしてもしなくても同じなので省略した。OnList関数とOnSeq関数の違いはListの関数を使うかSeqの関数を使うかだけ。
OnListの実行結果は以下の通り。
## OnList ## part 1 function list 1 function list 1 function list 1 part 2 function list 2 function list 2 function list 2 ABC1 DEF2 GHI3
目論んだ通り、リスト要素の回数だけ関数1と関数2が呼ばれている。
OnSeqの実行結果は以下の通り。
## OnSeq ## sect 1 function seq 1 function seq 1 function seq 1 sect 2 function seq 1 function seq 2 function seq 1 function seq 2 function seq 1 function seq 2 ABC4 DEF5 GHI6
関数1が2回ずつ呼ばれ、参照を使った順序数が変わってしまっている。この程度のコードで結果が変わるのである。
Seqは遅延評価されるのに加え、要素のインスタンスが常に1つという制限がある。で、
if Seq.length d1 > 2 then
のときにd1が評価され、分岐するのだが、関数2での評価時、もうd1の要素は保持されていない(要素のインスタンスは常に1つ)ため再度関数1からやり直している。ところが関数1は順序数を得るのに参照を使った代入をしている不純な関数なので結果が変化してしまうのだった。この関数1が純粋でも問題は同じ。関数1が重い処理だったりした場合、無駄にCPUと時間を食うことになる。不純な関数を使ったのは結果が変化するという、どう考えてもダメな結果を出すため。
if true then
というように分岐の判定であるd1の評価をやめると、関数1は要素数回のみ呼び出される。
ようはSeqには気を付けろということだ。.NETのコレクションがデフォルトではSeqでしか扱えないのは、どう考えても罠。強烈な罠。
2014.09.05
OSTRACISM CO.
OSTRA / Takeshi Yoneki