C# 4.0 メモ: Lazy<T> による遅延リストの作成

2012-07-26 公開, 2012-09-11 修訂 (鈴)

1. はじめに

"Groovy 応用: LINQ ライクなメソッドによるフィボナッチ数の計算" §8 ではフィボナッチ数を計算し表示するコード例を Ruby,Groovy とともに C# で示した。 それは yield return によるイテレータと LINQ メソッドを組み合わせたものであり,評価結果のメモ化をしない点で,伝統的な意味での遅延評価 (lazy evaluation) と異なっていた。

一方,C# 4.0 から (より正確には .NET Framework 4 から) は System.Lazy<T> クラス [microsoft.com] (以下,単に Lazy<T> クラス) が標準クラス・ライブラリに追加されている。 これは Scheme などに見られる遅延評価の約束 (promise) を C# に翻訳したものに相当する。 このクラスを使えば "Groovy 応用: 遅延評価によるフィボナッチ数の計算" で Groovy に対して示した方法と同じように伝統的な遅延評価を実現することができる。

執筆時現在,[microsoft.com] の Lazy<T> クラスの当該ページでは "lazy initialization" を「限定的な初期化」と訳しています。 そのため,これが遅延評価と関係していることが一見わかりにくいのですが,その実体は遅延評価の約束そのものです。
2012-09-11 追記: 追記時現在,当該ページは "lazy initialization" を「遅延初期化」と訳すように修正されています。

以下,本稿では,この Lazy<T> クラスをもとに作成した遅延リスト・クラスの例を示す。 その応用として遅延評価を利用して関数的に定義したフィボナッチ数列を与える。 最後に発展的な応用として,フィボナッチ数列で 1000 桁になる最初の項を求める例を示す。

以下のコード例では,C# 2.0 の代表的な機能だけ知っていればさしあたり理解できるように,C# 2.0 の時から人知れず用意されていて後の時代で重要になった機能や,C# 3.0 以降の新参の機能について,注釈で簡単に説明するようにしました。 ですから,まだ Windows 2000 が健在だった時代の .NET Framework 2.0 で時が止まっている人も,本稿の注釈をひととおり読むだけで現代の C# まで半分程度はキャッチアップできます。 【補】を先頭に置いて他の注釈と区別します。

2. Lazy<T> による遅延リスト LazyList の作成

下記に Lazy<T> をもとに作成した遅延リスト・クラスの例を示す。

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;

public class LazyList<T>: IEnumerable<T>
{
    public T Head { get; private set; }
    public Lazy<LazyList<T>> Tail { get; private set; }


    public LazyList(T head, Lazy<LazyList<T>> tail) {
        Head = head;
        Tail = tail;
    }

遅延リストとは残りの要素 (Tail) を約束とするリストである。 ここではそのとおりに定義する。

【補】 using System.Collections は,直接使わなくても,この例のように IEnumerable<T> の実装クラスを自分で定義するときは必要です。
【補】 public T Head { get; private set; } は T 型の公開プロパティ Head をデフォルト実装付きで定義します。 さしあたり,普通のデータ・メンバのように Head を使うことができます。 プロパティに対応する直接参照不可のデータ・メンバが暗黙のうちに定義されます。 ただし,ここでは set に private を付けていますから,代入操作はこのクラス内だけに限られます (付けなければクラス外から代入できます)。 あくまでプロパティですから,後日,実装を変更するとき,バイナリ互換性を保ったまま (つまり,他の部分の再コンパイルなしに) get や set の実装を自由に与えることができます。
    public LazyList<R> ZipWith<X, R> (LazyList<X> x, Func<T, X, R> binaryOp) {
        R head = binaryOp(Head, x.Head);
        var tail = LazyZip(Tail, x.Tail, binaryOp);
        return new LazyList<R> (head, tail);
    }

    private static Lazy<LazyList<R>> LazyZip<X, R>
        (Lazy<LazyList<T>> t, Lazy<LazyList<X>> xt, Func<T, X, R> binaryOp) {
        if (t == null)
            return null;
        return new Lazy<LazyList<R>> (() => {
                var v = t.Value;
                if (v == null)
                    return null;
                return v.ZipWith(xt.Value, binaryOp);
            });
    }

遅延リストに対するメソッド ZipWith を定義する。 後述 のフィボナッチ数の計算でこのメソッドを使う。

もう一つの遅延リスト x (要素型は X) と2引数関数 binaryOp (引数型は T と X, 結果の型は R) を引数とする。 二つのリスト thisx の要素を一つずつとって関数 binaryOp に適用した結果から構成される新しい遅延リスト (要素型は R) を返す。

補助メソッド LazyZip では,新しい遅延リストの残り要素である tail を作るために,U 型の戻り値を返す無引数関数を引数にとる Lazy クラスのコンストラクタ Lazy<U>(Func<U>) を使う (実際のコードでは U として LazyList<R> が使われている)。

遅延評価の約束 (promise) の値を得るには Value プロパティを使う。 つまり,Lazy クラスのインスタンス t に対して t.Vaule の値を参照する。 このプロパティは Scheme の force 手続きに相当する。 まだ約束がかなえられていなければ,ここでそれがかなえられる (つまり,そのインスタンスの構築時に指定されていた関数がようやく実行されて,その結果の値が返される)。 以前すでにかなえられていたならば,単にそのときの結果の値が再び返される。

補助メソッド LazyZipstatic メソッドとして定義するのは,ラムダ式の環境として暗黙のうちに thisx (および Headx.Head) への参照が残らないことを,C# 実装の詳細によらず確実に保証するためである。

【補】 Func<T, X, R> は T 型と X 型の2引数を受け取り R 型の結果を返すメソッドを表すために標準で用意されているデリゲート (つまり,関数型) です。 参照マニュアル [microsoft.com] では Func<T1, T2, TResult> のように型変数名が表記されています。
【補】 「仮引数並び => {文の並び}」という形式で無名関数を定義できます。 ラムダ算法の伝統にならい,これをラムダ式 (lambda expression) と呼びます。 無引数のラムダ式は「() => {文の並び}」のように仮引数並びに () を書いて表記します。 {文の並び} の中の return 文が返す値が TResult 型であるとき,この無引数ラムダ式の型は Func<TResult> になります。 上記のコードで使われているのは Func<LazyList<R>> 型の無引数ラムダ式です。
【補】 ローカル変数を宣言するとき,型を書くかわりに var で済ますことができます。 このとき,変数の型は右辺の式から静的に (つまり,コンパイル時に) 決定されます。 ですから型に関する矛盾がないかどうかは,型を陽に書いたときと同じく静的に厳しく検査されます。 コンパイルの結果,得られるコードも型を陽に書いたときと同じです。
    public IEnumerator<T> GetEnumerator() {
        return GetEnumeratorFor(this);
    }

    private static IEnumerator<T> GetEnumeratorFor(LazyList<T> x) {
        for (;;) {
            yield return x.Head;
            var t = x.Tail;
            if (t == null)
                yield break;
            x = t.Value;
            if (x == null)
                yield break;
        }
    }

IEnumerable<T> の実装クラスとして必要なジェネリック版 GetEnumerator() メソッドを定義する。

ループは,残りの要素が null か,または残りの要素を (Value プロパティの参照によって) 評価した値が null だったとき終わる。 補助メソッド GetEnumeratorForstatic メソッドとして定義するのは,ループが進行したとき,いつまでも this への参照が残らないことを,C# 実装の詳細によらず確実に保証するためである。

【補】 yield return や yield break を含むメソッドを呼び出したとき,その時点ではメソッド本体の文は実行されないことに注意してください。 それらのメソッドは,
  1. それらの文から構成されたイテレータである IEnumerator<T> オブジェクトを返すか,
  2. またはそのようなイテレータを生成する仮想的な「列」である IEnumerable<T> オブジェクトを返します。
どちらであるかはメソッドの戻り値型の宣言からコンパイル時に決定されます。 ここでは IEnumerator<T> と宣言していますから 1. に該当します。

一方,2. の場合は IEnumerator<T> を戻り値型とする GetEnumerator() メソッドを含む IEnumerable<T> 実装クラスがコンパイル時に暗黙のうちに定義され,そのクラスのインスタンス生成を行うメソッドが,宣言されたメソッド名で定義されます。 実例を "Groovy 応用: LINQ ライクなメソッドによるフィボナッチ数の計算" §8 にみることができます。
    static IEnumerable<long> Fib() {
        long a = 1;
        long b = 1;
        for (;;) {
            yield return a;
            long t = b;
            b = a + b;
            a = t;
        }
    }
この例では戻り値型を IEnumerable<long> と宣言しています。 IEnumerator<long> GetEnumerator() メソッドを含む無名の IEnumerable<long> 実装クラスが自動的に定義されます。 メソッド Fib() を呼び出すと,この無名の実装クラスのインスタンスが構築されて戻り値となります。
【補】 メソッド本体は,イテレータとして与えるべき「列」の要素の値を yield return 文で yield します (= もたらします)。 次の要素の値が求められたとき,yield return 文の次の文から実行を再開し,(次のループの) yield return 文を実行してその要求に応えます。 yield break 文でイテレータとしての動作を終了します。 初期の C# との互換性を保つため yield 1語では予約語扱いされません。

yield return や yield break を含むメソッドの動作ははじめは奇怪に思えるかもしれませんが,いくつかの例を読んで書いて実行すれば直観的に使えるようになります。 実行時のオーバーヘッドを心配する必要はありません。 メソッド本体の見かけは Ruby 等でいう内部イテレータと同じですが,実際に作られる IEnumerator<T> オブジェクトは外部イテレータとして動作します。 しかし,このとき内部イテレータを外部イテレータとして動作させる常套手段であるスレッドは使いません。 C# コンパイラはメソッド本体を,外部イテレータを構成する状態マシンへとコンパイル時に自動変換しますから,現実にはほとんどオーバーヘッドなく軽快に実行できます。
ZipWith メソッドと GetEnumerator メソッドは this などへの不要な参照を結果の値に残さないようにしています。 遅延リストのいくつかの応用では,処理の進行に伴って不要になった先頭要素を次々と捨てる必要があります。 その妨げとならないようにしたわけです。 ただし,本稿での応用範囲に限って言えば元々その必要はありません。 ですから,こうした考慮が無駄でないかどうかは,実はまだ実証していません……。
    IEnumerator IEnumerable.GetEnumerator() {
        return GetEnumerator();
    }

IEnumerable<T> の実装クラスとして必要な非ジェネリック版 GetEnumerator() メソッドを定義する。 その本体は単にジェネリック版の GetEnumerator() を呼び出すだけである。

【補】 普通,このメソッドは何も考えず定型的に上記のように定義します。冒頭部の using System.Collections も忘れずに!
このクラスに限って言えば return GetEnumeratorFor(this) と書いた方が,呼び出しの階層が一つ減ってわずかに効率的ですが,定型をあえて崩すほどの違いはありませんから,ここではそうしませんでした。
    public override string ToString() {
        var sb = new StringBuilder ();
        var x = this;
        sb.Append('(');
        for (;;) {
            sb.Append(x.Head.ToString());
            var t = x.Tail;
            if (t == null)
                break;
            if (t.IsValueCreated) {
                x = t.Value;
                if (x == null)
                    break;
                sb.Append(' ');
            } else {
                sb.Append(" . #");
                break;
            }
        }
        sb.Append(')');
        return sb.ToString();
    }
}

最後に,文字列化メソッドを定義する。

このメソッドは各要素の値を丸括弧で囲んで空白で区切った文字列を返す。 まだかなえられていない約束が残っていれば,末尾を . #) として返す。 約束がかなえられているかどうか (つまり,指定されていた関数が実行されているかどうか) は IsValueCreated プロパティで判定する。

この表記方法は "Simple Lazy Programs in Little Lazy Lisp" で使ったものを流用しました。

実際的な読みやすさ,書きやすさのため,下記のクラスも定義する。

public static class LazyListHelper
{
    public static LazyList<T> Cons<T> (T x, Lazy<LazyList<T>> y) {
        return new LazyList<T> (x, y);
    }

    public static Lazy<T> Delay<T> (Func<T> nullaryOp) {
        return new Lazy<T> (nullaryOp);
    }
}

これを使えば,new LazyList<int>(1, null) のかわりに Cons(1, null) と書くことができ,new Lazy<int>(() => 2) のかわりに Delay(() => 2) と書くことができる。

つまり,Scheme で (delay (+ 1 2)) と書くように C# で Delay(() => 1 + 2) と書けます。 ただし Scheme の (force x) にあたる式は x.Value と書きます。 十分に簡潔ですから,あえて Force メソッドは設けません。

使用例を示す。

using System;

using L = LazyListHelper;

class TestLazy
{
    static void Main(string[] args) {
        var x = L.Cons(10, L.Delay(() => L.Cons(20, null)));
        Console.WriteLine("type of x = {0}", x.GetType());
        Console.WriteLine("x = {0}", x);
        Console.WriteLine("tail of x = {0}", x.Tail.Value);
        Console.WriteLine("x = {0}", x);
    }
}
【補】 using L = LazyListHelper は右辺の別名として左辺を定義します。 このプログラムのなかで,型名に LazyListHelper と書くかわりに簡潔に L と書くことができるようになります。 左右が逆ですが C 言語の typedef と似た機能です。
【補】 「仮引数並び => { return 式; }」というラムダ式は簡単に「仮引数並び => 式」という形式で書くことができます。 説明の都合上,紹介の順序が逆になりましたが,むしろ,こちらのほうが本来のラムダ式です。

.NET 互換環境 Mono [mono-project.com] の Mono.framework 2.10.9_11 for OS X による実行例を示す。

01:~/tmp$ dmcs /t:library LazyList.cs
01:~/tmp$ dmcs /r:LazyList test-lazy.cs
01:~/tmp$ mono test-lazy.exe
type of x = LazyList`1[System.Int32]
x = (10 . #)
tail of x = (20)
x = (10 20)
01:~/tmp$  

Windows 上の Cygwin による実行例を示す。 "Life with Cygwin - 25.1" に示す方法で Microsoft Visual C# コンパイラ csc へのアクセスを用意してあるものとする

01:~/tmp$ csc /t:library LazyList.cs
Microsoft (R) Visual C# 2010 Compiler version 4.0.30319.1
Copyright (C) Microsoft Corporation. All rights reserved.

01:~/tmp$ csc /r:LazyList.dll test-lazy.cs
Microsoft (R) Visual C# 2010 Compiler version 4.0.30319.1
Copyright (C) Microsoft Corporation. All rights reserved.

01:~/tmp$ ./test-lazy.exe
type of x = LazyList`1[System.Int32]
x = (10 . #)
tail of x = (20)
x = (10 20)
01:~/tmp$  
Windows に .NET Framework 4 がインストールされていれば,Microsoft Visual C# 4.0 コンパイラもそれに含まれています。 この例では "Life with Cygwin 24" で説明したように環境を整備した Cygwin を使っていますが,最低限,ただのコマンド プロンプトでも OK です。

3. LazyList を使った遅延評価によるフィボナッチ数の計算

遅延リストを定義したから,今や "Groovy 応用: 遅延評価によるフィボナッチ数の計算" §4 に示した方法と同じように無限のフィボナッチ数列 fibs を与えることできる。

読者がリンク先を確かめる手間を省くために,下記に LazyExtension 付きの Groovy によるフィボナッチ数列の定義の主要部だけ示します。
  def fib;
  fib = [1G,
         [1G, delay {
             zipWith({ x, y -> x + y }, fib, fib[1])
           }]]
ここでは遅延リストを長さ2のリスト [ head, 約束 ] による cons セルで表現しています。 ただし,最初の cons セルだけは第2要素が約束ではなくリストになっています。 これで問題がない理由は LazyExtension を型に関して緩やかに作っておいたためですが,そもそもそう作ったわけは,実行前の静的型検査機能が弱い Groovy ではあらかじめ緩めにしておかないと,とても人の手に扱えなかったからです。 この緩やかさは,信頼できる型検査と自動的な型推論を備えた C# ではあえて真似すべきことではありません。

下記のプログラムでは LINQ の .Take(50).ToArray() を使って,無限の数列 fibs の先頭 50 個を配列 x にしている。 これが可能なのは,遅延リスト LazyList<T> を IEnumerable<T> インタフェースの実装クラスとして定義しているからである。

using System;
using System.Linq;
using System.Numerics;

using L = LazyListHelper;

class LazyFib
{
    static void Main(String[] args) {
        LazyList<BigInteger> fibs = null;

        fibs = L.Cons(1, L.Delay
                      (() =>
                       L.Cons(1, L.Delay
                              (() =>
                               fibs.ZipWith(fibs.Tail.Value,
                                            (a, b) => a + b)))));

        var x = fibs.Take(50).ToArray();
        Console.WriteLine("[" + String.Join(", ", x) + "]");
        Console.WriteLine(fibs);
    }
}
このプログラムに限って言えば,無限多倍長整数クラス System.Numerics.BigInteger ではなく 64 ビット長整数 long を使っても破綻しません。 long に置換して実験してみてください。 一方,System.Numerics.BigInteger を使うときは後述の実行例のようにコンパイル時に System.Numerics を参照先として追加する必要があります。
【補】 (a, b) => a + b は2引数のラムダ式です。 このように型を省略したときは,var の場合と同じく,文脈からコンパイル時に自動的に型が決定されます。 2引数と結果の値がどれも BigInteger と型推論されますから,ラムダ式の型は Func<BigInteger, BigInteger, BigInteger> となります。 あえて仮引数の型を省略せずに書けば (BigInteger a, BigInteger b) => a + b となります。
LINQ メソッドを使わずに foreach 文でループすることも,もちろんできます。 下記に差し替えるか,または追加して実験してみてください。
        int i = 0;
        foreach (var e in fibs) {
            Console.WriteLine(e);
            i++;
            if (i == 50)
                break;
        }

Mono.framework 2.10.9_11 for OS X による実行例を示す。 ただし,LazyList.cs前節の実験LazyList.dll にコンパイルしてあるものとする。

01:~/tmp$ dmcs /r:LazyList /r:System.Numerics lazy-fib.cs
01:~/tmp$ mono lazy-fib.exe 
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181
, 6765, 10946, 17711, 28657, 46368, 75025, 121393, 196418, 317811, 514229, 83204
0, 1346269, 2178309, 3524578, 5702887, 9227465, 14930352, 24157817, 39088169, 63
245986, 102334155, 165580141, 267914296, 433494437, 701408733, 1134903170, 18363
11903, 2971215073, 4807526976, 7778742049, 12586269025]
(1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765 10946 17711 
28657 46368 75025 121393 196418 317811 514229 832040 1346269 2178309 3524578 570
2887 9227465 14930352 24157817 39088169 63245986 102334155 165580141 267914296 4
33494437 701408733 1134903170 1836311903 2971215073 4807526976 7778742049 125862
69025 . #)
01:~/tmp$  

この結果を "Groovy 応用: LINQ ライクなメソッドによるフィボナッチ数の計算" §8 と比較しよう。

このプログラムでは最後に Console.WriteLine(fibs) を実行して,遅延リスト自体の ToString() メソッドによる文字列表現を表示している。 計算された値がメモ化されて保持され,51 番目以降が未評価の約束 (表示上は #) となっていることが分かる。

Windows 上の Cygwin による実行例を示す。 これも前節最後の実験で LazyList.csLazyList.dll にコンパイルしてあるものとする。

01:~/tmp$ csc /r:LazyList.dll /r:System.Numerics.dll lazy-fib.cs
Microsoft (R) Visual C# 2010 Compiler version 4.0.30319.1
Copyright (C) Microsoft Corporation. All rights reserved.

01:~/tmp$ ./lazy-fib.exe 
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181
, 6765, 10946, 17711, 28657, 46368, 75025, 121393, 196418, 317811, 514229, 83204
0, 1346269, 2178309, 3524578, 5702887, 9227465, 14930352, 24157817, 39088169, 63
245986, 102334155, 165580141, 267914296, 433494437, 701408733, 1134903170, 18363
11903, 2971215073, 4807526976, 7778742049, 12586269025]
(1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765 10946 17711 
28657 46368 75025 121393 196418 317811 514229 832040 1346269 2178309 3524578 570
2887 9227465 14930352 24157817 39088169 63245986 102334155 165580141 267914296 4
33494437 701408733 1134903170 1836311903 2971215073 4807526976 7778742049 125862
69025 . #)
01:~/tmp$  

4. フィボナッチ数列で 1000 桁になる最初の項を求める例

発展的な応用として,フィボナッチ数列で 1000 桁になる最初の項を求める例を示す。 ただし,第1項と第2項を 1 として数えるものとする。

using System;
using System.Numerics;

using L = LazyListHelper;

class LazyFib2
{
    static void Main(String[] args) {
        LazyList<BigInteger> fibs = null;

        fibs = L.Cons(1, L.Delay
                      (() =>
                       L.Cons(1, L.Delay
                              (() =>
                               fibs.ZipWith(fibs.Tail.Value,
                                            (a, b) => a + b)))));

        var target = BigInteger.Pow(10, 999);
        int i = 1;
        foreach (var e in fibs) {
            if (e >= target) {
                Console.WriteLine("F({0}) = {1}", i, e);
                break;
            }
            i++;
        }
    }
}

この例では,数の 10 進表現が 1000 桁に到達したかどうか,1 の次に 0 が 999 個続く数を BigInteger.Pow(10, 999) で作っておき,これ以上か未満かで判定している。 題意を,ちょうど 1000 桁でなければならず 1001 桁以上は認めないと解釈するならば,これは必要条件しか判定していない。 ただし,フィボナッチ数列に限れば,前項と前々項の和として項の値が求められるから,この判定だけで十分である。

LINQ メソッドを使うことも,もちろんできます。 using System.Linq をして foreach ループを下記に差し替え,x と result の値を表示してみてください。 ただし,LINQ の Where メソッドで列の要素の添字値 index を得るとき,最初の要素の添字値を 0 として数えた値が得られることに注意してください。
        int result = -1;
        var x = fibs.Where((e, index) => {
                if (e >= target) {
                    result = index;
                    return true;
                } else {
                    return false;
                }
            }).First();

Mono.framework 2.10.9_11 for OS X による実行例を示す。

01:~/tmp$ dmcs /r:LazyList /r:System.Numerics lazy-fib2.cs
01:~/tmp$ mono lazy-fib2.exe 
F(4782) = 1070066266382758936764980584457396885083683896632151665013235203375314
52060469404062188914758248979265780469488817759195748433646667256995951299603046
12627480924821861440694330512347744427502737817530875793916661921492591867595539
66422837148943113074699503439547001985432609723067290192870526447243726117715821
82554849112052501320147861296593138179223555965745203950613755146783754322911960
21299340482607061753977068470682028954869026661854351245219003694806413574474709
11707619766945691070098024393439617474103736912503231365532164773697023167755051
59517351846057995491941096777837322966579658164651390348815425631018422419025984
60880001101862555502454939371136516570394476295847145485234259504285824253060835
44435428212611008992863795048006894330309773217834864543113205765659868456288616
80871869383529735064398629764066000072356291790520705116407761481249188583094594
05666883391093509444565763576661516193177537928916615813271596168774879838218204
92520348473874384736771934512787029218636250627816
01:~/tmp$  
計算はほぼ瞬間的に終了します。 記述は省略しますが,Windows の .NET Framework 4 でも同様の結果が得られます。

5. おわりに

C# 4.0 より前の古い C# で伝統的な遅延リストを作ろうとした試みは Lazy Lists in C# [porg.es] と "The Virtues of Laziness" [msdn.com] にみることができる。 現在の Lazy<T> の直接の源流はおそらく Lazy Computation in C# [microsoft.com] である。

フィボナッチ数列で 1000 桁になる最初の項を求める問題は "Problem 25 - Project Euler" [projecteuler.net] に示されたものである。 C# による他の解としては "Problem25 フィボナッチ数列で1000桁になる最初の項番号" [geocities.jp] がある。

Haskell や L2Lisp ("New Little Lazy Lisp in Java") と異なり,C# では約束を透過的に使うことはできない。 C# の静的型付けは,約束である Lazy<T> を素の T と区別するからである。 しかし,その一方で,いつ Scheme の force に相当する演算をすればよいかについて悩む必要はない。 これは静的型検査のない Scheme で delay を利用して約束を作ったとき,よく悩まされる問題であり,L2Lisp で暗黙の force を導入した動機である。 C# では,コンパイラが型の不一致で不満を言わなくなるまで Lazy<T> インスタンスに .Value を付ければよい。

これは実体験にもとづく記述です。 最初にスケッチした (大まかには正しいはずの) プログラムの型の矛盾を,コンパイルが通るように無くすだけで,かなりの完成度になります。 型推論や別名定義の機能とあいまって C# の静的型付けのシステムは,うざい邪魔者ではなく真に助けになるというべきものになっています。 ただし,null に対する検査や場合分けの欠如,欠落については,現在の C# はあまり助けになりません。 おそらくそこに言語としての改善の余地があります。

約束による遅延リストであっても,IEnumerable<T> さえ実装すれば LINQ メソッドと組み合わせることができる。 前者は call-by-need 的な動作をし,後者は call-by-name 的な動作をするが,両者は決して相容れないものではない。 少なくとも次の形式で使うことができる。

   遅延リスト.LINQメソッド(…).LINQメソッド(…).….LINQメソッド(…)

このとき遅延リストはメモ化されたデータ源となる。 本稿では例によってこれを示した。

極論すれば,これからの C# では集合体を表すどんなクラスも LINQ と組み合わせられなくては価値がありません。 そのテストにひとまず合格したわけです。

Copyright (c) 2012 OKI Software Co., Ltd.