[C#][式木][LINQ] 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 を引数にしたもの
    • 例. IEnumerable<T>.Where(匿名メソッド)
  2. 式木を引数にしたもの
    • 例. IQueryable<T>.Where(ラムダ式)

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 について理解を深めることができるように思う。

.NETC#, LINQ, 式木

Posted by Fujiwo