Advent LINQ 2013 : LINQ連載の目次

丁度区切りがついたので、目次を付けておきます


Advent LINQ (1): SplitAsEnumerable
Advent LINQ (2): TextReader.AsEnumerable
Advent LINQ (3): FilesAsEnumerable
Advent LINQ (4): Buffering
Advent LINQ (5): Flatten
Advent LINQ (6): Match
Advent LINQ (7): Count
Advent LINQ (8): ElementAt
Advent LINQ (9): ParallelQuery対応拡張メソッドの実装
Advent LINQ (10): .NET 2.0でLINQを使う
Advent LINQ (11): SQL文を取得する
Advent LINQ (12): リファクタリング可能なメンバ名
Advent LINQ (13): ExpressionとLINQ
Advent LINQ (14): 条件式が実行される場所
Advent LINQ (15): SQL表現可能な式
Advent LINQ (16): 表形式と構造化形式
Advent LINQ (17) : Expressionの評価
Advent LINQ (18) : Expressionの探索
Advent LINQ (19) : Expressionのコンパイルと独立性
Advent LINQ (20) : IEnumerableとIQueryableの相互変換
Advent LINQ (21) : LINQプロバイダー
Advent LINQ (22) : 解釈するクエリの範囲
Advent LINQ (23) : 事前評価可能な式
Advent LINQ (24) : The Provider Core
Advent LINQ (25) : LINQプロバイダーの仕上げ

Advent LINQ 2013 : あとがき

Advent LINQ 2013 : おまけ (Parallel.ForEach)


以前の連載:

LINQは本当に強力だ (1) データ加工の究極の道具
LINQは本当に強力だ (2) クエリの連結と拡張メソッド
LINQは本当に強力だ (3) 拡張メソッドによる拡張
LINQは本当に強力だ (4) アルゴリズムヘルパーメソッド
LINQは本当に強力だ (5) クエリ構文とメソッド構文
LINQは本当に強力だ (6) TextFieldContext
LINQは本当に強力だ (7) 範囲変数と構文の選択
LINQは本当に強力だ (8) 何が並列化されるのか

Advent LINQ 2013 : おまけ (Parallel.ForEach)

Advent LINQ (10): .NET 2.0でLINQを使うの回で、比較的簡単にParallel.ForEachを自前で実装出来ると書いたので、例を見せる。
 
 

namespace System.Threading.Tasks
{
    public static class Parallel
    {
        public static void ForEach<T>(IEnumerable<T> enumerable, Action<T> action)
        {
            Debug.Assert(enumerable != null);
            Debug.Assert(action != null);

            var exceptionList = new List<Exception>();
            using (var wait = new ManualResetEvent(false))
            {
                var count = 1;
                foreach (var item in enumerable)
                {
                    Interlocked.Increment(ref count);

                    ThreadPool.QueueUserWorkItem(new WaitCallback(p =>
                    {
                        try
                        {
                            try
                            {
                                action(item);
                            }
                            catch (Exception ex)
                            {
                                lock (exceptionList)
                                {
                                    exceptionList.Add(ex);
                                }
                            }
                        }
                        finally
                        {
                            if (Interlocked.Decrement(ref count) == 0)
                            {
                                wait.Set();
                            }
                        }
                    }));
                }

                if (Interlocked.Decrement(ref count) == 0)
                {
                    wait.Set();
                }

                wait.WaitOne();
            }

            if (exceptionList.Count >= 1)
            {
                throw new AggregateException(exceptionList);
            }
        }

        public static void Invoke(params Action[] actions)
        {
            ForEach(actions, action => action());
        }
    }
}

コンパイルを通すには、他にもActionクラスが必要だ。このサンプルはVSCoverageToEmmaで実装したもので、以下の場所に完全なコードがある。

VSCoverageToEmma/VSCoverageToEmma.Core/Tasks/Parallel.cs

Advent LINQ 2013 : あとがき

むちゃくちゃ大変でした。

思い立ったのは11月の下旬、何だか周りでAdvent Calendarのひそひそ声が聞こえ始めた頃です。Advent Calendarをやった事が無かったので、「ふむ、25日まで1日ずつネタを書けばいいのか、簡単じゃないか。やってみるか、一人で」などと考えたのが悲劇の始まりだった。

まず、記事のさじ加減が分からない。「まぁ、LINQなら何でもネタがあるよね。そうだ、縛りを入れるか。一つ一つの記事がある程度独立していて、かつ、短く、読者が応用を想像できるような内容にするってのはどうだろうか?」

LINQは現場で、「当たり前に使っている」か、「全く使っていないか」の両極端に感じる。twitterで流れてくるものの中には、「LINQ禁止」とか、あり得ない現場もあるようだ。
そんなわけで、あんまり専門寄りに寄ってしまってもあれだから、少しでも底辺を持ち上げて、「LINQって何?」な人口を減らす事が出来たら良いなと思った。
(まあ、これは前の連載でも意識していたけど)

で、書き始めたのだが… すぐに壁にぶち当たった。

既に自分の中ではLINQが「空気」状態だった。「空気について説明する」が如く、フォーカスする対象が無いのだ。例えば、きわめて具体的なシチュエーションであれば、いくらでもLINQによる解決策を提示できる。しかし、具体例を示し過ぎると、読者が応用を想像しにくくなってしまう。あくまで、ネタの背景が分かるようにし、かつサジ加減を調整してあまり具体的過ぎる例にしないように(時にはわざとアホな方に振ったり)する必要があった。そして、それに耐えうるネタでなければならない…

10回目ぐらいまではパパッと思いついたネタも、その後が続かない。初めはばりばり書いて書き溜めておいたので良かったのだが、本業が忙しいこともあり、段々ストックが底をついてくる。金を貰っているわけでもないのにこのプレッシャー!!

そして、ふらふらと、やる気の無かったIQueryableネタをやり始めた。もうヤバい。何がって、この後は、アレだ。禁断のネタ「LINQプロバイダー」しかないじゃないか。

LINQプロバイダーなら、このネタだけで最後まで続けられるという点では、ネタ詰まりは回避できる。ただ、LINQプロバイダーは複雑すぎて、色々説明した後でないと、本題に入れない。これでは、1話毎に完結させるという縛りを満たすことがとても難しい。おまけに複雑さを低減するために、どうしても図を書かなければならない。

この迷い、15回ぐらいまで続く。少し読み返してみると、苦悩ぶりが伺えるw

さらに、NGK2013BでLT登壇したあおりを受け、あろうことか、JenkinsのAdvent Calendarにポチっとしてしまった… この状態で気がふれたとしか思えない。何故書く気になったのか、未だに良く分からない…

そして、ネタ考案の時間切れと半分ヤケとで、LINQプロバイダーをやらざるを得なくなり、腹を括って突入。書くからには、LINQ to SQLやLINQ to Entitiesがブラックボックス過ぎて理解不能と思っている方に向けて、少しでも「仕掛け」が見えるようにする必要があるだろうと。

AdventCalendarそんなわけで、後半は特に検証とかやったつもりですが、間違いがあったらゴメンナサイ。頑張って、強く縛りを意識して書いてみました。

右側のカレンダーを見ると、すげーなー、我ながら良く書いたよなぁ、と思います。
本日は23日の夜中、とりあえず、風呂入って寝ますw

Happy Merry Christmas!!

Advent LINQ (25) : LINQプロバイダーの仕上げ

前回のLocationFinderクラスによって、検索条件となる文字列の一覧が得られた。この検索条件をTerraServer WCFインターフェイスを使用して、サービスに送信する。すると、結果レコードがPlaceクラスに入れられて返される。結果が複数のレコードとなる可能性があり、WCFメソッドの戻り値はPlaceの配列で定義されている。
ExpressionTreeModifierPath
とうとう結果が得られたのだ。これをそのまま返せばよい。が、しかし、今まで扱ってきたのは、あくまでIQueryableによるLINQクエリだ。IEnumerableならそのまま返せばよい(配列はIEnumerableを実装しているので)が、少なくともIQueryableでなければならない。
であれば話は簡単だ。配列をAsQueryableすれば良い。

// Get the place name(s) to query the Web service with.
LocationFinder lf = new LocationFinder(lambdaExpression.Body);
List<string> locations = lf.Locations;
if (locations.Count == 0)
    throw new InvalidQueryException("You must specify at least one place name in your query.");
// Call the Web service and get the results.
Place[] places = WebServiceHelper.GetPlacesFromTerraServer(locations);
// Copy the IEnumerable places to an IQueryable.
IQueryable<Place> queryablePlaces = places.AsQueryable<Place>();

結果のIQueryableは、もはやTerraServer LINQプロバイダーの手を離れ、LINQ to ObjectsのLINQプロバイダーが処理を行うようになる。


まだ終わりではない

最初にInnerMostWhereFinderクラスで、最も内側のWhereを探索した事を覚えているだろうか? いま、一息ついたところだが、ここまでの処理で実現したのは、その「最も内側のWhere条件」だ。その外側のLINQクエリは放置されている。これを処理しなければならない。
しかし、前述のようにもうTerraServerは関係ないため、配列をAsQueryableした結果を使って続きのクエリを実行させる必要がある。これを行わせるのが、「ExpressionTreeModifier」クラスだ。このクラスもExpressionVisitorを継承している。やっていることは単純だ。TerraServerのコンテキストクラスのインスタンスを、上記配列をAsQueryableしたものに挿げ替える。
生成されたExpressionは、もはやTerraServerコンテキストを含まない。かつ、サービスから得られた結果を配列で含んでいるため、Expressionツリー全体がLINQ to Objectsで評価可能な状態にある。CreateQueryを呼び出してIQueryableを生成すれば、そのインスタンスはLINQ to ObjectsのLINQプロバイダーが管理する。
こうして、LINQ to TerraServerが完成した。

Advent LINQ (24) : The Provider Core

とうとう、LINQプロバイダーの中心部へと来た。今までIQueryableが収集したExpressionを探索したり変形したりしたが、結局まだ検索条件の解析をしていなかった。ここで「LocationFinder」クラスを使用して、検索条件の解析を行う。名前からはピンと来ないが、このクラスが中心的存在だ。
TheCorePath
このクラスは例によってExpressionVisitorクラスを継承しており、前述のInnerMostWhereFinderクラスとEvaluatorクラスによって得られたExpressionを探索する。
TerraServerサービスから返される結果は、「Place」クラスに格納される。Placeクラスはエンティティクラスであり、「Name」プロパティと「State」プロパティを持つ。TerraServerのサービスは、任意の場所を示す文字列を与えることで、その場所に応じたレコード群が返される。そこで、Placeクラスのプロパティにアクセスする式を書けば、それが検索条件として反映されるようにすればよい。

// コンテキストを生成
var terraPlaces = new QueryableTerraServerData<Place>();
// コンテキストに検索条件を与え、TerraServerサービスにクエリを発行し、結果を取得する
var query =
    from place in terraPlaces
    where place.Name == "Johannesburg"
    select place;

上記の例は、Nameプロパティが”Johannesburg”と一致するかどうかを確認している。同様に、

// Stateで検索
var query =
    from place in terraPlaces
    where place.State == "Kentucky"
    select place;

でも検索できるようにする。


BinaryExpressionの解析

上記のクエリ構文をメソッド構文、かつ拡張メソッドではない構文に直すと、以下のようになる。

// 拡張メソッドではないメソッド構文
var query =
    Queryable.Where(
        terraPlaces,
        place => place.Name == "Johannesburg");

これらとExpressionがどのように対応するかを示したのが、以下の図だ。
TheProviderCore
さて、ここで注意点がある。比較演算子の場合、右辺と左辺は入れ替えることが可能だ。つまり、

// コンテキストに検索条件を与え、TerraServerサービスにクエリを発行し、結果を取得する
var query =
    from place in terraPlaces
    where "Johannesburg" == place.Name    // 右辺と左辺が逆
    select place;

のようなクエリを記述しても、これを許容しなければならない。BinaryExpressionクラスには「Right」と「Left」プロパティがあり、演算子の右辺と左辺のExpressionに対応している。従って、これらに設定されているExpressionが逆であっても、正しく識別しなければならない。
サンプルコード中では「ExpressionTreeHelpers」クラスの「IsMemberEqualsValueExpression」メソッドや「GetValueFromEqualsExpression」メソッドが該当する。逆に定義されていても問題がないように実装されている事が分かる。
まとめると:

  1. 二項演算子(BinaryExpression)であり、
  2. 二項の片方がPlace.Name、又はPlace.Stateプロパティであり、
  3. もう片方が固定値(Constant)

であれば、この固定値が検索したい値と言う事になる。LocationFinderクラスはこのような探索を行い、用意しておいたリストに加える。
全てのExpressionノードを探索すれば、リストに検索条件となる値群が抽出されていることになる。

Advent LINQ (23) : 事前評価可能な式

IQueryableで表現されるクエリに、事前評価可能な式が含まれることがある。

using (var context = new AdventureWorksLT2012_DataEntities())
{
    // 検索条件を変数で与える
    var selectType = "Shipping";
    var addresses =
        from customerAddress in context.CustomerAddress
        where customerAddress.AddressType == selectType
        select customerAddress;

}

上記のselectTypeは、LINQクエリ中の条件式に与えられる。これはメソッド構文で言う所のラムダ式に変換されるため、このローカル変数は暗黙のクロージャーのメンバーフィールドとして定義される。この式のExpressionをダンプすると、以下の結果が得られる。

<Call value="value(System.Data.Entity.Core.Objects.ObjectQuery1[ShowQueryInLinqToEntities.CustomerAddress]).MergeAs(AppendOnly).Where(customerAddress =&gt; (customerAddress.AddressType == value(ShowQueryInLinqToEntities.Program+&lt;&gt;c__DisplayClassd).selectType))">
    <Null />
    <Call value="value(System.Data.Entity.Core.Objects.ObjectQuery1[ShowQueryInLinqToEntities.CustomerAddress]).MergeAs(AppendOnly)">
        <Constant value="value(System.Data.Entity.Core.Objects.ObjectQuery`1[ShowQueryInLinqToEntities.CustomerAddress])" />
        <Constant value="AppendOnly" />
    </Call>
    <Lambda value="customerAddress => (customerAddress.AddressType == value(ShowQueryInLinqToEntities.Program+<>c__DisplayClassd).selectType)">
        <Equal value="(customerAddress.AddressType == value(ShowQueryInLinqToEntities.Program+<>c__DisplayClassd).selectType)">
            <MemberAccess value="customerAddress.AddressType">
                <Parameter value="customerAddress" />
            </MemberAccess>
            <MemberAccess value="value(ShowQueryInLinqToEntities.Program+<>c__DisplayClassd).selectType">
                <Constant value="value(ShowQueryInLinqToEntities.Program+<>c__DisplayClassd)" />
            </MemberAccess>
        </Equal>
        <Parameter value="customerAddress" />
    </Lambda>
</Call>

少々長い(そして、ノード毎の出力がいい加減なので醜い)が、Equalノード下に二つのMemberAccessノードがあり、片方はcustomerAddress.AddressTypeを示している。と言う事は、もう片方のConstantノードが、クロージャーのメンバーフィールドを示していると言えそうだ。
このLINQクエリは、例によって「人類の英知」を使えば、以下のように単純化できる。

using (var context = new AdventureWorksLT2012_DataEntities())
{
    // 検索条件をクエリの式中に直接埋め込む
    var addresses =
        from customerAddress in context.CustomerAddress
        where customerAddress.AddressType == "Shipping"
        select customerAddress;
}

こうすると:

<Call value="value(System.Data.Entity.Core.Objects.ObjectQuery1[ShowQueryInLinqToEntities.CustomerAddress]).MergeAs(AppendOnly).Where(customerAddress =&gt; (customerAddress.AddressType == &quot;Shipping&quot;))">
    <Null />
    <Call value="value(System.Data.Entity.Core.Objects.ObjectQuery1[ShowQueryInLinqToEntities.CustomerAddress]).MergeAs(AppendOnly)">
        <Constant value="value(System.Data.Entity.Core.Objects.ObjectQuery`1[ShowQueryInLinqToEntities.CustomerAddress])" />
        <Constant value="AppendOnly" />
    </Call>
    <Lambda value="customerAddress => (customerAddress.AddressType == "Shipping")">
        <Equal value="(customerAddress.AddressType == "Shipping")">
            <MemberAccess value="customerAddress.AddressType">
                <Parameter value="customerAddress" />
            </MemberAccess>
            <Constant value=""Shipping"" />
        </Equal>
        <Parameter value="customerAddress" />
    </Lambda>
</Call>

Equalノード配下の二つ目のノードが、MemberAccessから直接的なConstantの文字列に置き換わっている。
さて、最適化の概要を見たが、このような事が出来れば良い事は分かるが、実際必要なのだろうか? 今回の最適化作業は前回と異なり、「行わなければならない」最適化となる。もしこれを実行しないと、LINQプロバイダーがExpressionを処理する際に、「customerAddress.AddressType == selectType」という式をサービス側に解釈可能な形で変換しなければならない。selectTypeはクロージャーのメンバーフィールドだ。サービスはこのクロージャーのメンバーフィールドに「リモート」から直接アクセス出来るだろうか?
もちろん、出来る訳がない。出来ないとしたら、取りうる手段は以下の二つだ。

  • クエリのエラーとする。
    但し、エラーとしてしまうと、クエリを非常に簡潔に書かなくてはならないという制約が生じる。上記のような、ちょっとした(ローカル変数への参照)式でさえ、NGとなってしまう。これは厳しい制約だ。
  • 事前にこのフィールドを解釈し、サービスに提供出来る検索条件にする。
    もし、注目している式がConstantな値(固定値)に変換出来るのであれば、あらかじめ変換してしまえば良い。上記の例のように、ローカル変数への参照(クロージャーのメンバーフィールドへの参照)を事前に解釈して、単なる「Shipping」文字列に出来てしまえば、サービス側に送ることが可能となる。

そういったわけで、Constantに事前変換可能な式を見つけ出して、変換する事が必要となる。この操作を行うのは、「Evaluator」クラスだ。このクラスは内部に「Nominator」クラスと「SubtreeEvaluator」クラスを含む。どちらもExpressionVisitorクラスを継承していて、Expressionの探索を行う。

最初のステップ (Nominator)

BeforePartialEval1000
「Nominator」クラスは、Expressionを探索して、Expressionノードの末端(Leaf)から逆順で、Constantに変換可能かを見る。変換可能かどうかは、そのExpressionノードが「Parameter」かどうかで判断する。Parameterでなければ変換可能とみるが、ノードのチェインのどこかにParameterノードが存在すれば、そのノードより上(Root側)のノードは、全て変換不可とみなされる。
Parameterノードが含まれているツリーの枝は、何らかのメソッド呼び出しを実行しないと、結果が得られない可能性がある。しかし、LINQクエリ中にメソッド呼び出しが記述されていると言う事は、単にサービスの向うの機能を仮想的に表現したものであるかもしれない。そうであれば、ローカル環境で実行しても無意味となるので、実行してConstantに変換する事は出来ない。
このような判断を下しつつ、変換可能なExpressionノードをHashSetに収集する。
#上の図では、変換不可とみなされたノードにチェックを付けている。
#また、HashSetに収集されるExpressionを、〇で囲った。

変換のステップ (SubtreeEvaluator)

AfterPartialEval1000
Nominatorで収集したExpression群は変換可能候補のリストであるので、SubtreeEvaluatorクラスの探索でこのリストをチェックし、該当するノードを見つけたらそのノード以下をConstantに変換する。既にConstantであるExpressionやnullのExpressionは無視する。
変換方法は簡単だ。まず、そのExpressionをラムダ式に変換する(Expression.Lambdaメソッドを使う)。次に、そのLambdaExpressionをコンパイルし、出来上がったデリゲートをコールする。すると、実際に式が実行されて、式の評価結果が固定値として返って来る。あとは、Expression.Constantメソッドで、Expressionに変換すれば完了だ。
ExpressionVisitorは、探索だけではなく、Expressionツリーの変更もサポートする。本来、Expressionはイミュータブル(変更出来ないクラス・例えばSystem.Stringのような)なので変更出来ないが、Visitメソッドの戻り値として新しいExpressionを返すことで、Expressionのツリーを変更(サブツリーの置き換え)する事が出来る。
探索だけ行う場合は、元のExpressionを返せば、式の変形は行われない。なので、Expression.Constantで新しいExpressionを作ったら、単にそれを返却すればよい。これで注目している式をConstantに置き換える事が出来た。

Advent LINQ (22) : 解釈するクエリの範囲

LINQプロバイダーがクエリを解釈する範囲を定める必要がある。以下のLINQクエリについて考えてみる。

var r = new Random();
// 乱数から、5で割り切れる数を抽出
var div5 =
    from index in Enumerable.Range(0, 100000)
    let value = r.Next()
    where (value % 5) == 0
    select value;
// 更に3で割り切れる数を抽出
var div5and3 =
    from value in div5
    where (value % 3) == 0
    select value;

これは「人類の英知」によって、以下のクエリに変形できる。

// 乱数から、5と3で割り切れる数を抽出
var div5and3 =
    from index in Enumerable.Range(0, 100000)
    let value = r.Next()
    where ((value % 5) == 0) && ((value % 3) == 0)
    select value;

最初のクエリ例は、そのまま実行される事になる。注目するのは、クエリ全体でwhere条件が2回実行されることだ。人間がこのクエリを見れば、等価な後の例に変形する事が出来る。そして、LINQクエリがIQueryableによるExpressionであった場合、記述通りの式が得られる。つまり、where条件が2回記述された式だ。後の例に変形する必要がある場合、それはLINQプロバイダーの責任となる。
TerraServerのサンプル例では、このような最適化を諦めている。つまり、仮にwhere条件が2回以上現れても、最適化を行わない。実際にはどのような結果となるだろうか?
この挙動を決定しているのが、「InnerMostWhereFinder」クラスだ。
InnerMostWhereFinderの話をする前に、クエリとExpressionの対応を再確認しよう。

// LINQ to Entitiesによるクエリの例
using (var context = new AdventureWorksLT2012_DataEntities())
{
    var addresses =
        from customerAddress in context.CustomerAddress
        where customerAddress.AddressType == "Shipping"
        select customerAddress;
}

もう何度も出ている例だが、実はまだこれをメソッド形式で例示していない。

// メソッド形式に変更
using (var context = new AdventureWorksLT2012_DataEntities())
{
    var addresses =
        context.CustomerAddress.
        Where(customerAddress => customerAddress.AddressType == "Shipping");
}

select句はそのまま射影しているだけなので省く。ところでこれは、C#の拡張メソッドで記述している。そのため、「本当のところのメソッド呼び出し」と同じではない。更に書き換えると、以下のようになる。

// 本当のメソッド呼び出し
using (var context = new AdventureWorksLT2012_DataEntities())
{
    var addresses =
        Queryable.Where(
            context.CustomerAddress.
            customerAddress => customerAddress.AddressType == "Shipping");
}

Where拡張メソッドは、元々Queryableクラスのstaticなメソッドだ。第一引数はIQueryableのインスタンス(context.CustomerAddress)、第二引数がWhere条件を示すラムダ式(そして、これをExpressionで受ける)だ。
つまり、結果の「addresses」は、上記のQueryable.Where呼び出しがExpressionとして表現されたもの、となる。確かめたい場合は、addressesがIQueryableなので、Expressionプロパティを見てみると良い。
さて、仮にWhere条件が2つに分かれていたら、どうだろうか。

// Where条件が2つ存在する
using (var context = new AdventureWorksLT2012_DataEntities())
{
    // Shippingとマークされた住所を抽出
    var addresses =
        Queryable.Where(
            context.CustomerAddress.
            customerAddress => customerAddress.AddressType == "Shipping");
    // 更にそこから2003年以降のものを抽出
    var addresses2003 =
        Queryable.Where(
            addresses.
            customerAddress => customerAddress.ModifiedDate >= new DateTime(2003, 1, 1));
}

これを、Expressionとして分かりやすくするために、一つの式にまとめる(Where条件は分けたまま)。

// Where条件が2つ存在する
using (var context = new AdventureWorksLT2012_DataEntities())
{
    // Shippingとマークされた住所を抽出し、更にそこから2003年以降のものを抽出
    var addresses2003 =
        Queryable.Where(
            Queryable.Where(
                context.CustomerAddress.
                customerAddress => customerAddress.AddressType == "Shipping"),
            customerAddress => customerAddress.ModifiedDate >= new DateTime(2003, 1, 1));
}

ここまで変形すれば、Expressionがどのような構造になっているのか、何となく想像がつくと思う。Whereメソッドの呼び出しはネストしていて、「内側」の条件式と「外側」の条件式は、明確に分かれている。
さて、TerraServerのウェブサービスで利用できる検索機能は、LINQクエリのそれと比べて非常に単純なものとなる。恐らく殆どのストレージサービスは、LINQクエリの検索条件の柔軟性と比べて、著しく劣るはずだ。逆に、SQL ServerとLINQ to SQL(またはLINQ to Entitites)とを比較すると、LINQクエリの検索条件の表現力は劣る。Transact-SQLで記述できるクエリははるかに柔軟であり、LINQクエリで表現可能な範囲は狭い。
このギャップをどのように埋めるかと言う事を考える必要がある。TerraServerに絞って考えるなら、仮にLINQクエリで必要以上に複雑な検索条件を指定された場合、それをどこまで再現するかだ。
最初の例で示したように、人間が柔軟に考えるのと同じような最適化を、Expressionを探索する事によって実現する事は不可能ではない。ただ、それは相当大変なことだ。
そこで(かどうかは分からないが)、TerraServerのサンプルでは大胆に割り切っている。LINQクエリの全体の式から、最も内側の検索条件を探し出し、その条件だけを解釈してサービスに送る、というものだ。そして、サービスから結果が返ってきたら、その後はLINQ to Objectsで処理させる、つまりインメモリで絞り込ませる。
そのために、Expressionのツリーから、最も内側のWhere句の位置を探し出す必要がある。この処理を行うのが、「InnerMostWhereFinder」クラスだ。このクラスはExpressionVisitorを継承し、与えられたExpressionからWhere句(Whereメソッド)を探し出す。そして、最も内側にあったWhere句呼び出しのExpressionを記憶しておき、探索が終わったらそれを返す。
もし、この動作に不満があるなら、人間の手で単一のWhere句に置き換えればよい。人間にやらせるか、または複雑な最適化まで含めて自動的に処理を行わせるか、という選択肢が考えられるが、私ならサンプル通り妥協しても良いかと思う。
#Where句の合成だけなら、AND条件として処理すれば良い。しかし、ネストしたExpressionが
#上記のように単純であるとは限らない。その部分まで担保するのはかなり大変だ。

Advent LINQ (21) : LINQプロバイダー

IQueryableには、何らかのLINQプロバイダーが関連付けられている。AsQueryableを使用してIQueryableに変換したクエリは、以下のように「EnumerableQuery<T>」というLINQプロバイダーが割り当てられている。
LinqProvider
IQueryableインターフェイスとLINQプロバイダーの関係や、プロバイダーの作成方法については、チュートリアル : IQueryable LINQ プロバイダの作成にまとまっている。やや難解であるので、読み合わせ的に補足していこうと思う。


想定しているシナリオ

「TerraServer-USA Web Services」というサービスが存在したとして(実際あるようだが、何故か応答が返ってこない)、そのウェブサービスに対してクエリを発行し、結果を取得出来る。何もない状態では、WCFで定義されたインターフェイスに従って、メソッド呼び出しのような形式でサービスを利用するが、このサービスに対応するLINQプロバイダを書けば、LINQクエリの記述でサービスを利用する事が可能になる。つまり、既存のWCFインターフェイスを使用して、「LINQ to TerraServer」を作ると言う事になる。
TerraServerWebService


全体的な構造

LINQクエリを記述する側は、IEnumerableやIQueryableといったインターフェイスにだけ注目する。LINQプロバイダーを作る場合(そして、LINQ to SQLやLINQ to Entities)は、IQueryableインターフェイスを実装した独自のクラスを用意する。と同時に、LINQプロバイダーとなる「IQueryProvider」インターフェイスを実装したクラスも用意する。このクラスが、LINQクエリーのExpressionを解釈し、SQL文を生成したりする。今回はWCFを使用して、TerraServerのサービスにアクセスする。
OverallClasses
また、LINQクエリの記述を成立させるには、結果を格納するエンティティとなるクラスが必要だ。例えば、

using (var context = new AdventureWorksLT2012_DataEntities())
{
    // CustomerAddressエンティティクラスが受け皿となるクエリ
    IQueryable<CustomerAddress> persons =
        from customerAddress in context.CustomerAddress
        where customerAddress.AddressType == "Shipping"
        select customerAddress;
}

このようなクエリが(RDBやWCFを通じてサーバーで)実行され、結果が返される。その結果をこのクラス(CustomerAddress)に格納する。そのため、クラスの定義が必要となる。
もう一つ、LINQ to SQLやLINQ to Entitiesでは、「コンテキスト」と呼ばれるRDBへの接続を管理するクラスも必要だ。これがないと、LINQソースとなる端点(上記の例であれば、context.CustomerAddress)が提供出来ないので、クエリが書けない。但し、別の方法もある。普通の列挙子をAsQueryableでIQueryableに変換した場合は、コンテキストとなるクラスは存在しない。つまり、何らかの方法で、LINQソースとなる端点が提供できれば良いと言う事だ。

Advent LINQ (20) : IEnumerableとIQueryableの相互変換

IEnumerableとIQueryableの相互変換は可能だ。その意味について考えてみる。

var r = new Random();
// 1: IEnumerable<int>をIQueryable<int>に変換する
IEnumerable<int> randomDatas =
    from index in Enumerable.Range(0, 100000)
    select r.Next();
IQueryable<int> queryableIntDatas = randomDatas.AsQueryable();
// 2: IQueryable<Customer>をIEnumerable<Customer>に変換する
using (var context = new AdventureWorksLT2012_DataEntities())
{
    IQueryable<Customer> queryableCustomers =
        from customer in context.Customer
        select customer;
    IEnumerable<Customer> enumerableCustomer = queryableCustomer.AsEnumerable();
    // IEnumerable<Customer> enumerableCustomer = (IEnumerable<Customer>)queryableCustomer;
}

先に2番目の変換を見てみる。
IQueryable<T>は、IEnumerable<T>を継承している。そのため、2番目の変換は、単にキャストするのと変わらない(AsEnumerableの実装はキャストしているだけだ)。
IEnumerable<T>にキャストしてforeach等で列挙した場合、IQueryable<T>の実装が持つGetEnumerator()を呼び出していることになる。これは擬似的に、以下のようなコードだ。

// queryableの列挙子を取得する擬似コード
public static IEnumerable<T> PseudoAsEnumerable<T>(this IQueryable<T> queryable)
{
    // queryableで示されるクエリを実行し、結果群が返される(GetEnumeratorの呼び出し)
    foreach (var value in queryable)
    {
        yield return value;
    }
}

LinqGate
queryableは普通の列挙子ではない。LINQ to SQLやLINQ to Entitiesで示される、何らかのストレージとのインターフェイスを抽象化したものだ。そのため、このforeachによって何が引き起こされるのかは、queryableが何を対象にしているのかによって異なる。LINQ to SQLやLINQ to Entitiesでは、RDBに対してSQL文が発行され、結果が返って来るだろう。そのレコード群がforeachで列挙される。
QueryableObjects
では1番目の変換はどうだろう。
randomDatasはLINQ to Objectsのクエリ式だ。IQueryableで表現するためには、このクエリ全体が「記述的」になる必要がある。つまり、randomDatasクエリ式を「呼び出す式」がIQueryableで表現される。
IQueryableがGetEnumeratorによって列挙される時、randomDatasクエリ式を解釈する「LINQプロバイダー」が動作し、プロバイダーがrandomDatasクエリ式を列挙する。
当然ながら、この例ではRDBにアクセスするわけではないので、queryableIntDatasは「SQL文」を表している訳ではない。以下はデバッガのスナップショットだ。
IQueryableQuickWatch
IQueryableインターフェイスには、「Expression」プロパティが存在する。このプロパティは、例の「Expression」だ。IQueryableが表現するクエリ式全体が、このExpressionプロパティから公開される。そこで、このExpressionがどうなっているのか、Expressionの探索で示したFlatDumpExpressionVisitorを使ってXMLにダンプすると、以下のような単一のノードが返される。

<Constant value="System.Linq.Enumerable+WhereSelectEnumerableIterator`2[System.Int32,System.Int32]" />

結局、IQueryableが指す参照先は、IEnumerableのベールに包まれたrandomDatasであり、固定値(Constant)として認識されている。同じIQueryableを基礎としているのに、これは明らかにLINQ to SQLやLINQ to Entitiesとは異なる。
この違いは、LINQソースから生まれる。LINQ to Entitiesの例では、AdventureWorksLT2012_DataEntitiesクラスのCustomerプロパティからクエリを開始している。AsQueryableは、LINQ to Objectsのクエリを変換する。この違いによって、IQueryableの評価を受け持つ「LINQプロバイダー」が異なり、SQL文に変換されたり、列挙子の呼び出しに変換されたりする。

Advent LINQ (19) : Expressionのコンパイルと独立性

Expressionを取得するには、ラムダ式から生成するのが簡単だ。他にExpressionクラスに定義されている静的メソッドを使用して生成する事もできる。どちらにしても、LINQ to Objectsで使ってきたデリゲート(Func<T, U>)と異なり、Expressionを実行する事は出来ない。あくまでExpressionVisitorを使うなどして、式の構造を解析できるだけだ。しかし、ラムダ式に対応するLambdaExpressionは、「コンパイル」が可能だ。

// Expressionを生成
Expression<Func<bool>> func12 = () => "ABCDEFG".StartsWith("ABC") == true;
// Expressionをコンパイルし、デリゲートを生成
var func12Compiled = func12.Compile();
// 実行する
var result = func12Compiled();

「コンパイル」とは、実際にコンパイルされることだ。コンパイルされたデリゲートを使えば、任意の式を高速に実行出来る。便利そうであるが、実は複雑な問題が絡んでいる。Expressionを生成した際にあまり意識していないと思うが、Expressionのノードには「クロージャ」が含まれる場合がある。これは、ラムダ式を使うときには自然な概念として理解されていると思う。

public static Func<string> CreateClosure()
{
    // 暗黙のクロージャ(デリゲートを返す)
    var localVariable = 123;
    return () => localVariable.ToString();
}

localVariableはCreateClosureメソッドのローカル変数だ。常識的には、ローカル変数の内容は、メソッドを抜ける時に破棄される。上のコードではローカル変数を参照する「デリゲート」が、メソッドから返される。メソッドから返されたデリゲートは、果たして安全に実行できるだろうか?
C言語でローカル変数へのポインタを返してバグを招いたことは無いだろうか?しかし、上記のコードは問題なく動作する。localVariableが定数だから問題ないのだろうか?仮に定数ではなくても、このコードは正しく動作する。何故か?コンパイル時に生成される「クロージャクラス」のメンバフィールドが、localVariableに対応するように、コードが変更されてコンパイルされるからだ。
以下はこれを概念的に示したものだ。

// 不可視でコンパイラが自動的に生成するクロージャクラス
internal sealed class TemporaryClosureClass
{
    public int localVariable;
    public string Execute()
    {
        return localVariable.ToString();
    }
}
public static Func<string> CreateClosure()
{
    // 暗黙のクロージャ(デリゲートを返す)
    var closure = new TemporaryClosureClass();
    closure.localVariable = 123;
    return closure.Execute;
}

CreateClosureが返すデリゲートには、暗黙にTemporaryClosureClassクラスのインスタンスへの参照が含まれる。そのため、このインスタンスがGCに回収されることはなく、CreateClosureを抜けてからでも、問題なく実行する事が出来る。
では、デリゲートではなくExpressionならどうだろうか?

public static Expression<Func<string>> CreateClosure()
{
    // 暗黙のクロージャ(Expression)
    var closure = new TemporaryClosureClass();
    closure.localVariable = 123;
    return closure.Execute;     // <-- 構文エラー
}

残念ながら、「closure.Execute」はラムダ式ではないので、このコードはコンパイルして検証出来ない。しかし、式がMethodCallExpressionであるとして想像すると、次のようになるはずだ。

  • Method: TemporaryClosureClassクラスのExecuteメソッドを示す。
  • Arguments: Executeの引数(引数なしなので、空の配列)。
  • Object: closure変数への参照、つまりTemporaryClosureClassクラスのインスタンス。

このExpressionを前回のXML出力に掛けた場合、正しくXMLに変換できるだろうか?問題があるとすれば、Objectプロパティ、つまり、TemporaryClosureClassのインスタンスをどうやってXMLに変換するかだ。このようなクラスは、当然ToStringの明示的な実装を行っていない。そのため、残念ながら可読可能な状態での変換は望めないことになる。
そして、Expressionは暗黙的に、何らかのインスタンスに対しての参照を内包している可能性があることになる。そのため、Expressionを「コンパイル」出来たとしても、参照を内包している限りそれはその場限りでしか使えず、再利用して高速化するのは難しい。もし、再利用したい場合は、クロージャや外部インスタンスへの参照を暗黙に含まないように、注意してExpressionを組み立てる必要がある。