遅延評価:解説編

間違ったC#の使い方を追求してる気がするけど、IEnumerableとyieldを使うとなぜかあっさりと関数型言語風に使えてしまうことがおもしろいのでまだ突き進みます(^^;
きっとこれは偶然じゃなくてLINQの基礎なのかも?と思ったり(調べてないけど)。

たらいまわしでやってみたお手軽遅延評価を、いったん立ち止まって中身を詳しく見てみます。yieldはコンテナを順に返すのが普通の使い方なので、yieldで遅延評価ができるのはわかったけど、からくりがいまいちしっくりきません。

たらいまわしはやってることがややこしいので(ついでに意味がわからない)、単純なサンプルで見てみます。まずは遅延評価しない例。


class Before
{
static void Main()
{
int result = Funny( 1, HeavyId( 2 ) );
Console.WriteLine( result );
Console.Read();
}

static int HeavyId( int id )
{
Console.WriteLine( "Sleep" );
System.Threading.Thread.Sleep( 10000 );
return id;
}

static int Funny( int x, int y )
{
if ( x != 0 )
return x;
else
return y;
}
}

MainでFunnyに渡す2つ目の引数は、実行に時間が掛かるHeavyIdの結果です。しかし、Funnyは引数xが0のときだけyの値を必要とします。MainではFunnyの結果が欲しいだけで、この例ではHeavyIdの実行は必要ないわけです。そう見えなくても、百歩譲ってそういうことにしてください。

で、これをyieldを使った遅延評価版にしてみます。


class After
{
static void Main()
{
int result = LazyFunny( 1, LazyHeavyId( 2 ) );
Console.WriteLine( result );
Console.Read();
}

static IEnumerable<int> LazyHeavyId( int id )
{
Console.WriteLine( "Sleep" );
System.Threading.Thread.Sleep( 10000 );
yield return id;
}

static int LazyFunny( int x, IEnumerable<int> ey )
{
if ( x != 0 )
return x;
else
return Eval<int>( ey );
}

static T Eval<T>( IEnumerable<T> enumerable )
{
IEnumerator<T> it = enumerable.GetEnumerator();
if ( it.MoveNext() )
return it.Current;
else
throw new ArgumentException( "ゴラァ" );
}
}

たらいまわしたときのEvalを一般化したEvalを追加、HeavyIdのreturnをyield returnに変更、戻り値も変更、Funnyの引数yの型をintからIEnumerableにして値が必要なときにEvalで取り出すようにしました。
このように、お手軽遅延評価への変更は簡単です。
でも、普通はFunnyの中で必要なときにHeavyIdを呼ぶように変更しますね。そのほうが実行効率がはるかに良いです。こういうやり方もあるんだということです。欠点は実行効率ですが、利点もあって、設計時の理想のシンプルな形(つまり改造前のサンプル)に見た目が近い!これは(理想的なシンプルな)設計と実装の乖離が激しい手続き型言語と、設計と実装がほとんど同じに見える関数型言語の違いを物語っているのかもしれません。もしかして全然そうじゃないかもしれません(^^;

このサンプルをReflectorで見てみます。yieldはクラスに展開されます。ということを知ってることが前提条件で書いてます。なので、肝心のMoveNext()とCurrentだけを見ます。

まずはCurrent。

int IEnumerator<int>.Current
{
      [DebuggerHidden]
      get
      {
            return this.<>2__current;
      }
}

ここでは変数<>2__currentを返すだけでした。
次、MoveNext()。
状態マシンなのはyieldの展開後のクラスを見たことがあればご存知の通り。厳密すぎて手軽にyieldを使う気になれないのもご存知の通り… (--;

private bool MoveNext()
{
      switch (this.<>1__state)
      {
            case 0:
                  this.<>1__state = -1;
                  Console.WriteLine("Sleep");
                  Thread.Sleep(0x2710);
                  this.<>2__current = this.id;
                  this.<>1__state = 1;
                  return true;

            case 1:
                  this.<>1__state = -1;
                  break;
      }
      return false;
}

LazyHeavyIdメソッドの中身はここにありました!コンテナを順に返すような場合と同じと言えば同じですが、改めてこうやって見てみると面白いですね。

以下余談。
でも、MoveNext()で実行されるということは、以下のようなLengthを書いたときに、中身が評価されてしまうってことですね。


static int Length<T>( IEnumerable<T> e )
{
IEnumerator<T> it = e.GetEnumerator();
int i = 0;
while ( it.MoveNext() )
i++;
return i;
}

ちょっとおいしくないなぁ。