読者です 読者をやめる 読者になる 読者になる

プログラミング C# - 翔ソフトウェア (Sho's)

C#/.NET/ソフトウェア開発など

IQueryable な Twitter のタイムライン クラスと LINQ プロバイダー

C#

C# Advent Calendar 2014」の12日目の記事。

前の記事 ← → 次の記事

以前、「[C#][式木][LINQ] Hokuriku.NET C# 勉強会『C# 式木』(2014-10-26、金沢) のスライド公開」で、IQueryable な LINQ について解説した。

  1. LINQ to Objects 復習
  2. IQueryable<T>
  3. 式木 (Expression Tree)
  4. 式木メタ プログラミング
  5. LINQ プロバイダー

本記事では、その中の IQueryable なサンプルを補足する。

IQueryable な LINQ の中はどのようになっているのだろうか。

試しに少し実装してみることで、LINQ について理解を深めよう。

IEnumerable と IQueryable

[C#][ラムダ式][LINQ][式木] 匿名メソッドとラムダ式の違い」で紹介したように、匿名メソッドdelegate としてしか使えないが、ラムダ式delegate としても式木としても使うことができる。

[C#][ラムダ式][式木] Expression として扱えるラムダ式と扱えないラムダ式」で紹介したように、ラムダ式であれば必ず式木として使うことができるわけではない。

※ クエリ構文は、「式木として扱えるラムダ式」の糖衣構文。つまり、式木を扱うことになる。

参考: LINQ でのクエリ構文とメソッド構文 (C#) - MSDN

LINQ の中には、次の二つの種類のライブラリがある。

  1. delegate を引数にしたもの
  2. 式木を引数にしたもの

LINQ to Objects などは前者で処理され、LINQ to SQLLINQ to Entities などは後者だ。

IQueryable なものを作ってみよう

今回は、IQueryable な Twitter のライムライン クラスを作ろうとしてみる。

先ずは IQueryable なクラス QueryableTweets。

※ IQueryable なだけでは OrderBy の対象となることができないので、ここでは IQueryable からの派生で OrderBy 可能な IOrderedQueryable を用いることにする。

// QueryableTweets.cs

using System.Linq;

// IOrderedQueryable<string> な QueryableTweets
// まだ IOrderedQueryable インタフェイスを実装してないのでコンパイル エラー
public class QueryableTweets<TElement> : IOrderedQueryable<TElement>
{}

ここから、Visual Studio でインタフェイスの実装を行うと次のようになる。

// QueryableTweets.cs

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

// IOrderedQueryable<string> な QueryableTweets
// Visual Studio でインタフェイスを実装した直後
public class QueryableTweets<TElement> : IOrderedQueryable<TElement>
{
    public Type ElementType
    {
        get { throw new NotImplementedException(); }
    }

    public Expression Expression
    {
        get { throw new NotImplementedException(); }
    }

    public IQueryProvider Provider
    {
        get { throw new NotImplementedException(); }
    }

    public IEnumerator<TElement> GetEnumerator()
    { throw new NotImplementedException(); }

    IEnumerator IEnumerable.GetEnumerator()
    { throw new NotImplementedException(); }
}

IQueryable は IEumerable から派生している。そのため IEumerable のメンバーである GetEnumerator() を実装する必要がある。

その他に、ElementType、Expression、Provider というプロパティを実装しなければならない。

 実装を進めていこう。このクラスの実装はそれほど大変ではない。

Provider プロパティのために IQueryProvider インタフェイスを持つクラスを用意する必要があるが、ここでは、それを仮に TwitterQueryProvider クラスとしておこう。 TwitterQueryProvider クラスは後述する。

// QueryableTweets.cs

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

// IOrderedQueryable<string> な QueryableTweets
public class QueryableTweets<TElement> : IOrderedQueryable<TElement>
{
    public Type ElementType
    {
        get { return typeof(TElement); }
    }

    public Expression     Expression { get; set; }
    public IQueryProvider Provider   { get; set; }

    public QueryableTweets()
    {
        Provider = new TwitterQueryProvider(); // IQueryProvider インタフェイスを実装したクラス。後述。
        Expression = Expression.Constant(this);
    }

    public IEnumerator<TElement> GetEnumerator()
    { return ((IEnumerable<TElement>)Provider.Execute(Expression)).GetEnumerator(); }

    IEnumerator IEnumerable.GetEnumerator()
    { return GetEnumerator(); }
}

IQueryProvider なもの (LINQ プロバイダー) を作ろうとしてみよう

続いて、上記 QueryableTweets で使うための、IQueryProvider なものの実装だ。

これは、LINQ プロバイダーと呼ばれるもので、式木としてのクエリーを解釈する。

こちらの実装は大変だ。

クラス名を TwitterQueryProvider として、IQueryProvider を実装していこう。

// TwitterQueryProvider.cs

using System.Linq;

// LINQ プロバイダーの実験用
// まだ IQueryProvider インタフェイスを実装してないのでコンパイル エラー
public class TwitterQueryProvider : IQueryProvider
{}

ここから、Visual Studio でインタフェイスの実装を行うと次のようになる。

// TwitterQueryProvider.cs

using System;
using System.Linq;
using System.Linq.Expressions;

// LINQ プロバイダーの実験用
// Visual Studio でインタフェイスを実装した直後
public class TwitterQueryProvider : IQueryProvider
{
    public IQueryable CreateQuery(Expression expression)
    { throw new NotImplementedException(); }

    public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
    { throw new NotImplementedException(); }

    public object Execute(Expression expression)
    { throw new NotImplementedException(); }

    public TResult Execute<TResult>(Expression expression)
    { throw new NotImplementedException(); }
}

少し実装を進めてみる。

// TwitterQueryProvider.cs

using System;
using System.Linq;
using System.Linq.Expressions;

// LINQ プロバイダーの実験用
public class TwitterQueryProvider : IQueryProvider
{
    IQueryable IQueryProvider.CreateQuery(Expression expression)
    { return null; }

    public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
    { return new QueryableTweets<TElement> { Provider = this, Expression = expression }; }

    public TResult Execute<TResult>(Expression expression)
    { return default(TResult); }

    public object Execute(Expression expression)
    {
        // ここで式木を解釈して、コレクションを作って返す
        return null; // とりあえずは仮に null を返すだけにしておく
    }
}

この中で、ポイントとなるのは Execute メソッドだ。

この Execute メソッドには、式木が渡ってくる。この式木を解釈してやって、そこからコレクションとしての結果を返してやれば良い。

ここは後で実装することにして、とりあえずは null を返すだけにしておく。

ExpressionVisitor の派生クラスで Visitor パターンによる式木の解釈

LINQ プロバイダーの Execute メソッドでの式木を解釈だが、それには、ExpressionVisitor というクラスが使える。

ExpressionVisitor から派生することで、Visitor パターンによる解析が可能となる。

参考: ExpressionVisitor クラス - MSDN

LINQ のための式木をきちんと解釈するのは、かなり大変なことだ。

ここでは、ごく一部の構文にだけ注目して、そこのみに対応することにする。

取り敢えずの最低限のサンプル コード、Where(text => text.Contains("C#")) の形にのみ対応してみる。

尚、この中では、Twitter のタイムラインを取得する TwitterTimeline クラスを使っているが、 TwitterTimeline クラスは後述する。

// TwitterExpressionVisitor.cs

using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

// Visitor パターンで式木を解析して検索用文字列を取り出し、それを使って Twitter のタイムラインを取得する
public class TwitterExpressionVisitor : ExpressionVisitor
{
    public IEnumerable<string> Statuses { get; private set; }

    // 取り敢えずの最低限のサンプル コード
    // Where(text => text.Contains("C#")) の形にのみ対応してみる
    protected override Expression VisitMethodCall(MethodCallExpression expression)
    {
        // もし Where メソッドを呼ぶ式だったら
        if (expression.Method.Name == "Where") {
            // Where メソッドの第二引数であるラムダ式を取り出す
            var lambdaExpression = (LambdaExpression)((UnaryExpression)(expression.Arguments[1])).Operand;
            // そのラムダ式の Body 部を取り出す
            var bodyExpression = lambdaExpression.Body as MethodCallExpression;
            // もし Contains メソッドを呼ぶ式で
            if (bodyExpression != null && bodyExpression.Method.Name == "Contains") {
                // その引数が定数式だったら
                var constantExpression = bodyExpression.Arguments[0] as ConstantExpression;
                if (constantExpression != null) {
                    // その定数の値を検索文字列とし
                    var searchText = constantExpression.Value as string;
                    if (searchText != null)
                        // TwitterTimeline クラス (後述) を使って、タイムラインからその検索文字列にあたる Status を取得しておく
                        Statuses = new TwitterTimeline().Filter(searchText).Select(status => status.Text);
                }
            }
        }
        return base.VisitMethodCall(expression);
    }
}

TwitterTimeline クラスによる Twitter タイムラインの取得

次に、Twitter のタイムラインを取得するためのダミー クラス TwitterTimeline を用意する。

実際に Twitter のタイムラインを取得するコードを用意すれば良いわけだが、今回は説明の簡略化のために CoreTweet というライブラリとダミー コードを用いることにする。

CoreTweet は、Visual Studio から NuGet でインストールできる。

NuGet で CoreTweet のインストール
NuGet で CoreTweet のインストール

ダミー コードは次の通り。

// TwitterTimeline.cs

using CoreTweet; // Twitter のタイムライン取得用
using System.Collections;
using System.Collections.Generic;
using System.Linq;

// Twitter のタイムライン取得用
// CoreTweet ( https://github.com/CoreTweet/CoreTweet/wiki/Home(%E6%97%A5%E6%9C%AC%E8%AA%9E) ) を利用
// NuGet でインストールできる
// Twitterの開発者向けサイト "Twitter Developers" ( https://dev.twitter.com ) にアプリケーションの登録をし、
// Consumer Key、Consumer Secret、Access Token、Access Secret を取得するなどすれば、
// 実際に Twitter のタイムラインから取得することも可能
class TwitterTimeline : IEnumerable<Status>
{
    public IEnumerable<Status> Filter(string searchText)
    {
        // ダミー実装
        // 実際には、ここで searchText にマッチする Status のみを取ってくるのが良い
        return this.Where(status => status.Text.Contains(searchText));
    }

    public IEnumerator<Status> GetEnumerator()
    {
        // "Twitter Developers" に登録し、キーやトークンを取得すれば、実際に Twitter のタイムラインを取れる
        //var tokens = CoreTweet.Tokens.Create("[Your Consumer Key]", "[Your Consumer Secret]",
        //                                     "[Your Access Token]", "[Your Access Secret]");
        //return tokens.Statuses.HomeTimeline().GetEnumerator();

        // ダミー実装
        // 実際には Twitter のタイムラインを取ってくる
        yield return new Status { Text = "C# で CoreTweet を使って Twitter のタイムラインを取得してみた" };
        yield return new Status { Text = "式木いじりは茨の道" };
        yield return new Status { Text = "C#LINQ を使う" };
        yield return new Status { Text = "Hokuriku,NET C# 式木" };
    }

    IEnumerator IEnumerable.GetEnumerator()
    { return GetEnumerator(); }
}

LINQ プロバイダー TwitterQueryProvider への組み込み

では、TwitterQueryProvider に TwitterExpressionVisitor を組み込んでみよう。

Execute メソッドの中で、TwitterExpressionVisitor の Visit を呼ぶ。 すると、TwitterExpressionVisitor が式木を解釈し、結果を Statuses に入れるので、それを返せば OK だ。

// TwitterQueryProvider.cs

using System;
using System.Linq;
using System.Linq.Expressions;

// LINQ プロバイダーの実験用
public class TwitterQueryProvider : IQueryProvider
{
    IQueryable IQueryProvider.CreateQuery(Expression expression)
    { return null; }

    public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
    { return new QueryableTweets<TElement> { Provider = this, Expression = expression }; }

    public TResult Execute<TResult>(Expression expression)
    { return default(TResult); }

    public object Execute(Expression expression)
    {
        // ここで式木を解釈して、コレクションを作って返す
        var expressionVisitor = new TwitterExpressionVisitor();
        expressionVisitor.Visit(expression);
        var statuses = expressionVisitor.Statuses;
        return statuses.AsQueryable<string>();
    }
}

テスト

使ってみよう。

コンソール アプリケーションの Main から使用してみる。

// Program.cs

using System;
using System.Linq;

class Program
{
    static void Main()
    {
        IQueryable<string> query1 = new QueryableTweets<string>();
        IQueryable<string> query2 = query1.Where(text => text.Contains("C#"));

        // ここまでは、式木をつくっているだけ
        Console.WriteLine(query2.Expression);
        Console.WriteLine();

        // 下の foreach 中で実際に値を item に取り出そうとすると、
        // 1. TwitterQueryProvider の Execute にその式木が渡され、
        // 2. TwitterExpressionVisitor でそれが解析される中で、
        // 3. TwitterTimeline がタイムラインの取得を行う

        foreach (var item in query2)
            Console.WriteLine(item);
    }
}

実行結果は、次の通りだ。

value(QueryableTweets`1[System.String]).Where(text => text.Contains("C#"))

C# で CoreTweet を使って Twitter のタイムラインを取得してみた
C#LINQ を使う
Hokuriku,NET C# 式木

始めに式木が表示され、その後で、クエリーの結果 "C#" が含まれる Status が三行表示された。

このサンプルコードの範囲ではうまく動いたようだ。

■ 今回のまとめ

今回は、Hokuriku.NET C# 勉強会『C# 式木』 で説明した内容を補足した。

IQueryable なものを書き LINQ に対応させるのはかなり大変だが、その意味するところだけでも理解していけば、LINQ について理解を深めることができるように思う。