ラムダ式と型推論 (C# 3.0)

前回 (id:NyaRuRu:20070623:p1) の続きをちょっとだけ.

フォローアップ

続きにいく前にフォローアップ.
id:siokoshou さんの コメント にあるように,前回のコードサンプルは,一番最初のコードがベータ 2 以降でエラーとなるようです.
紹介していただいた以下の記事にあるように,匿名型の同値性と hashcode に関するセマンティクスを使いやすくする目的で,匿名型インスタンスのプロパティから public setter をのぞいてしまうことが決まったとのことでした.

新しい NewExpression の説明を読んでいて,ちょっとおもしろい記法を思いついたのですが,beta 2 がリリースされたら試してみましょう.
また前回述べた,匿名型を含む Generic Type を作る hack ですが,まさに同じことをあつかった先例が上の記事から紹介されていました.

前回記事の復習

まずは前回の復習から.
ラムダ式は便利ではあるのですが,メソッドのローカル変数に束縛するためには特殊な hack を行う必要がありました.以下は前回記事からの引用です.

static Func<T, T> Identity<T>(T dummy)
{
    return delegate(T arg){return arg;} ;
}

var person = new {ID=1, Name="Alice"};

// func の型は Func< {int:ID, string:Name}, {int:ID, string:Name} > と確定
var func = Identity(person);

// func の型が確定しているので,ラムダ式を何型と判断するかも定まる
func = (x => new { x.ID, Name = x.Name + "_" });

なぜ推論が失敗するのかをもう少し

これはどういうことかというと,ラムダ式から互換する .NET の型が多すぎて,ローカル変数の型を絞り込めないのです.

var dbl = x => x * 2; // コンパイルエラー

この var を,たとえば次のどれに置き換えてもコンパイルは成功します.

  • Converter<int, int>
  • Func<double, double>
  • Expression<Func<double, double>>

では x の型を明示すればよいのかというと,それでも曖昧性が残ります.

var dbl = (int x) => x * 2; // コンパイルエラー
  • Converter<int, int>
  • Func<int, int>
  • Expression<Func<int, int>>

これらのうちどの型がもっとも優先されるべきという基準は定義されていないので,やはりコンパイルエラーになってしまいます.結局,自由度の高すぎるローカル変数では,これ以上の絞り込みができない,と C# コンパイラがギブアップしてしまうのです.

メソッド引数としてラムダ式の型を制限する

一方,ラムダ式をメソッドの引数に書いた場合は状況が変わってきます.

MyClass.Foo( x => x * 2 );

Foo というメソッドが示されたことで,引数の位置に来る型は一定の制限が加わりました.
たとえば,以下のようなメソッドが定義されていて,かつ曖昧なオーバーロードが存在しなければ,ラムダ式の型は Expression<Func<int, int>> であると推論できます.

public static class MyClass
{
    public static void Foo(Expression<Func<int, int>> expr)
    {
        ...
    }
}

MyClass.Foo( x => x * 2 ); // OK

ジェネリックメソッドの引数として型を制限する

さらに,Generic Method の型推論と組み合わせることもできます.

public static class MyClass
{
    public static void Foo<TArg0, TResult>(Expression<Func<TArg0, TResult>> expr)
    {
        
    }
}

MyClass.Foo((int x) => x * 2); // OK
MyClass.Foo<int>(x => x * 2); // OK

ここで,TArg0 に曖昧性が残らないように,何らかの形で TArg0 を教えてやることが必要でした.一方で,TArg0 が決まればラムダ式の戻り値の型 TResult は推論できるので,明示する必要はありません.
興味深いのは,別の引数を通じても TArg0 に制限を加えられることです.

public static class MyClass
{
    public static void Foo<TArg0, TResult>(TArg0 dummy, Expression<Func<TArg0, TResult>> expr)
    {

    }
}

MyClass.Foo(1, x => x * 2); // OK. 最初の引数は,TArg0 を決めるためだけに用いている.

皆さん LINQ を使っていても,あちこちにラムダ式の型を明示しなければならなかったという印象は受けていないと思いますが,それは背後で型推論が縦横に型のマッチングをはかってくれているおかげではないでしょうか.

まとめ: ラムダ式をどこに書くか

ラムダ式をもっともシンプルな形 (省略した形) で書くためには,メソッド引数のような「型が制限される場所」に書くことがポイントとなります.

いろんな型が書けるローカル変数より,渡せる型が制限されているメソッド引数の方が,型推論を活用した省エネ typing では有利

また,このことから,var キーワードを用いたローカル変数にラムダ式を束縛するヒントも見えてきます.

ラムダ式から Expression Trees への型推論

ラムダ式を Expression Trees としてローカル変数に束縛するときは,例えば以下のように書くことになるでしょう.右辺で引数の型を明示しなくてもよいのは,左辺から自動的に推論が行われるためです.

Expression<Action<int>> pr = x => Console.WriteLine(x);
Expression<Func<int>> one = () => 1;
Expression<Func<int, int>> dbl = x => x * 2;
Expression<Func<double, double, double>> mul = (x, y) => x * y;
Expression<Func<double, double, double, bool>> odr = (x, y, z) => x <= y && y <= z;

今まで見てきた理由により,このままでは var キーワードに置き換えることはできません.そこで,Generic Method を用いてラムダ式の型を制限します.

public static class ExprUtl
{
    public static Expression<Action<TArg0>> Lambda<TArg0>(Expression<Action<TArg0>> expr) { return expr; }
    public static Expression<Func<TResult>> Lambda<TResult>(Expression<Func<TResult>> expr) { return expr; }
    public static Expression<Func<TArg0, TResult>> Lambda<TArg0, TResult>(Expression<Func<TArg0, TResult>> expr) { return expr; }
    public static Expression<Func<TArg0, TArg1, TResult>> Lambda<TArg0, TArg1, TResult>(Expression<Func<TArg0, TArg1, TResult>> expr) { return expr; }
    public static Expression<Func<TArg0, TArg1, TArg2, TResult>> Lambda<TArg0, TArg1, TArg2, TResult>(Expression<Func<TArg0, TArg1, TArg2, TResult>> expr) { return expr; }
}

ExprUtl.Lambda には,Expression Trees を引数にとるオーバーロードのみが定義されています.従って,このメソッドの引数に書いたラムダ式は,型推論によって Expression Trees が選択されるようになります.

var pr = ExprUtl.Lambda((int x) => Console.WriteLine(x));
var one = ExprUtl.Lambda(() => 1);
var dbl = ExprUtl.Lambda((int x) => x * 2);
var mul = ExprUtl.Lambda((double x, double y) => x * y);
var odr = ExprUtl.Lambda((double x, double y, double z) => x <= y && y <= z);

もういちどまとめ: そして LINQ の一文はなぜあんなに長いのか?

Expression Trees を束縛したローカル変数について,上に書いた一般的な記法のどちらがよいかは難しいところです.この記法では文頭がすっきりしている一方,一般的な記法は右辺が省略形で書けていてきれいです.
もっとも,簡潔さと使いやすさのバランスを考えれば,先ほども述べたように,うまく型を制限したメソッドの引数としてラムダ式を書く機会が最も多くなることでしょう.
ラムダ式を無理にローカル変数へ束縛しようとせず,メソッド連鎖の中にまとめて書いてしまった方が,むしろ冗長な型名を書かなくて済むというわけです.
これを突き詰めたのが,つまり LINQ の Standard Query Operators ということになります.なぜ LINQ を使って書いたコードの一文があんなに長いのか,なんとなくおわかりいただけたでしょうか?