F# の Seq に驚く

.NET の一級市民と「なるべく」設計されたという F#。一級市民になれたのだろうか。Visual Studio 2013 Community でも最初から F# が使える。がしかし、GUI プログラム (WPF プログラム) が作れるようにはなっていない。拡張プログラムの「F# Empty Windows App (WPF)」をインストールすることで GUI プログラムが作れるようになる。喜んで使い始めるが、どうもプロパティで GUI コントロールをバインドする部分がちゃんと動かない。ツリー表現のコントロール(パス)でアイテムを選べるのだが、アイテム名が表示されないので何を選んでるのかわからない。選ぶと XAML ファイルが変更されるので動いてはいるようだ。これがバグなのか、たまたま微妙に動いてるだけなのかわからない。F# は正規には WPF プログラムを作れるようにはなってないのだからたまたま微妙に動いてるだけという可能性は高い。なんにせよ結局 XAML は手で触らないといけない。一級市民ねぇ。

プロパティをイベントモードにすると「イベントハンドラーを追加する前に、分離コードファイルとクラス定義を追加してください。」などとお願いされちゃう。分離コードってなんだ? と調べると、C# の partial class を使って XAML の定義と C# の定義を融合するといったことをするようだ。問題は F# には partial class なんて概念がないこと。将来的にはサポートされるのかもしれないが、VS2013 の F#(バージョン3.1.2) には存在しない。困ったぞ。しかたがないので、アプリケーションのインスタンスを表す App.xaml を使うのを止め、コードでアプリケーションの初期化を記述した。イベントハンドラはコードで追加。まさしく手で追加。困った一級市民だ。

そんな、皆さんのディスクの肥やしになっているだろう 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 でしか扱えないのは、どう考えても罠。強烈な罠。