特集:Road to LINQ

C#で解説する「データ処理の直交化と汎用化」

岩永 信之
2011/06/14
Page1 Page2 Page3

LINQ to ……

 これまでの説明では、データの取得元をIEnumerable<T>インターフェイスに限っていた。IEnumerable<T>インターフェイスを使うと、データの扱い方に以下のような制約がかかってしまう。

  • pull型: プログラムがデータを能動的に取り出す。
  • 内部実行: データはプログラム内で加工する。

 そして、データの取得方法としては、この逆も考えられるのである。

  • push型: 外部から送られてくるデータを受動的に受け取る。
  • 外部クエリ: データの加工を外部システムに行ってもらう。

 .NET Frameworkには、push型データ取得用のIObservable<T>インターフェイス(System名前空間)と外部クエリ用のIQueryable<T>インターフェイス(System.Linq名前空間)が用意されている。

 ここで、今回のテーマ的に気になる点は、Figure 11に示すようなが必要なのか、Figure 12に示すようなだけでデータ処理できるのかである。

Figure 11  通りの実装
同じデータ処理を行いたくても、それぞれ別の実装が必要なのか。

Figure 12  通りの実装
異なるインターフェイスでも同じ実装でデータ処理が行えるのか。

 LINQはFigure 12のを実現する。といっても、インターフェイスが異なるため、完全にコードを共通化できるわけではないのだが、同じような書き方ができる。少なくとも、学習コストは大幅に下がるだろう。

push型データ取得

 push型データ取得の典型例は、携帯電話のキャリア・メールである。携帯電話側から能動的にメールを取りにいっているのではなく、サーバから送られてくるメールを受動的に受け取るのだ。

 また、最近の電子機器は、GPS(Global Positioning System: 全地球測位システム)など、さまざまなセンサを搭載している。通常、センサAPIでは、プログラムが能動的にセンサの状態を調べるのではなく、受動的にセンサからデータを受け取ることになる。

 このようなpush型データ取得(=受動的な待ち受け)は、携帯型機器において特に重要になるだろう。能動的な動作は電力消費が激しく、バッテリ駆動の携帯型機器には不向きなのである*2

 push型のデータ、すなわち、IObservable<T>インターフェイスに対するLINQを提供するのが「Reactive Extensions(通称、Rx)」*3というライブラリだ。上記のサンプルでは、説明も兼ねてRx相当の機能の一部を「再発明」しているのだが、ここではRxを使った例をList 13に示す。説明を簡単化するためにタイマーでデータをpushしているが、この部分はサーバからの通知だったり、センサ入力だったりに適宜読み替えてほしい(ちなみに、ObservableはSystem.Reactive.Linq名前空間のクラスである)。

*2 そのため、Windows Phone 7には、「Push Notification(プッシュ通知)」というサーバからデータを送りつける仕組みが用意されていたり、Reactive Extensions(通称、Rx)が標準搭載されていたりする。
*3 長らく研究的位置付けのプロジェクトだったが、2011年4月に「製品レベル」として正式リリースされた。

// 別スレッドで500ミリ秒間隔でデータをpush通知する。
var source = Observable.Interval(TimeSpan.FromMilliseconds(500));
 
// 別スレッドから来たpush通知を受け取って、LINQでデータ加工。
// IEnumerable版と同じ形式の拡張メソッドを用意することで利用者の学習コストを下げる。
var o = source.Where(x => (x % 2) == 1).Select(x => x * x);
 
// 加工結果を購読して、コンソールに表示。
o.Subscribe(x => { Console.WriteLine(x); });
 
// 別スレッドに処理が移っているので、待たないとMainメソッドを抜けてしまう。
System.Threading.Thread.Sleep(5000);
List 13 push型で取得したデータに対するLINQ

外部クエリ

 「外部システムに記憶されたデータを取得してきて処理を行う」という場面は非常に多い。RDBMS(Relational Database Management System)からのデータ取得が典型的な例だろう。

 Figure 13に示すように、IEnumerable<T>インターフェイスを使っても外部システムからのデータ取得はできなくはないのだが、データを全件返すという部分がネックになる。外部システムとの接続はネットワークを介することが多く、通信量の増大が性能の劣化に直結する。

 一方、IQueryable<T>インターフェイスを使う場合、加工依頼(これを「クエリ(query)」と呼ぶ)を送り、外部システム上でデータ加工まで行ってもらい結果だけを受け取る。クリエの典型例は、RDBMSに対するSQL文だ。

Figure 13 外部システムからのデータ取得
IEnumerable<T>インターフェイスを使う場合とIQueryable<T>インターフェイスを使う場合での差異。加工まで外部システムに任せるかどうかの差がある。

 LINQでも、LINQ to SQLやLINQ to Entitiesなど、いわゆるO/Rマッパーが有名なので、「外部システム」の部分にRDBMSを思い浮かべがちだろう。もちろん、代表例ではある。

 List 14にLINQ to Entitiesの利用例を示そう。

using (var context = new SampleDataContext())
{
  // 見てのとおり、IEnumerableに対する書き方とまったく同じように書ける。
  // でも、ちゃんとSQL文になって、データベース・サーバ上でクエリが実行される。
  var q = context.Parents
    .Where(x => x.Children.Sum(y => y.Value) >= 500)
    .Select(x => x.Name);
 
  foreach (var x in q)
  {
    Console.WriteLine(x);
  }
}
List 14 LINQ to Entitiesの利用例
SampleDataContextクラスのParentsプロパティの型はIQueryable<T>インターフェイスである。

 しかし、O/RマッパーだけがLINQではない。例えば、最近では、REST(Representational State Transfer)形式でのHTTPを介したデータ取得を行えるシステムが増えている。この場合にも同様の通信量問題があり、その回避策としてデータ加工の条件を渡して加工結果だけを返してもらう場合が多い。

 また、Hadoopのような分散データ処理も、この「外部システムへのクエリ」の一種と見なせるだろう。IQueryable<T>インターフェイスを使って実現できる。

 LINQと関連して、以下のような製品もある。

  • ODataREST形式でデータ取得を行うためのプロトコル。ODataサービスは、Visual Studioから「サービス参照」することで、IQueryable<T>インターフェイスを介したデータ取得が行える。
  • Windows HPC Server マイクロソフト製の分散データ処理環境。LINQを使ってWindows HPC Server上の分散データ処理を行うための「LINQ to HPC」というライブラリも提供されている。

クエリ式

 よく、「LINQに慣れてくると、foreachなどのループを書く量が減る」といわれる。なぜかというと、LINQはループ相当の機能を持っているからである。List 8のforeach文が、List 12のSelect/Whereメソッドに置き換わるわけである。元のループと比べての利点は以下のとおりだ。

  • 処理を“軸”で直行分解できる
  • パイプライン型の書き方になる
  • push型データ処理や外部クエリにも応用が利く

 しかし、1つ問題もある。2重ループに相当するコードを書くのが難しい。例えば、List 15のようなデータ処理を考えてみよう。

foreach (var mj in data.MajorCategories)
{
  foreach (var mn in data.MinorCategories)
  {
    if (mj.Id == mn.MajorCategoryId)
    {
      var line = string.Join(", ", mj.Name, mn.Name);
      Console.WriteLine(line);
    }
  }
}
List 15 2重ループでデータ処理する例

 このような、2重ループに相当するLINQクエリにはSelectManyメソッドを使う。List 16のように書けばよい。

var q = data.MajorCategories
  .SelectMany(mj => data.MinorCategories, (mj, mn) => new { mj, mn })
  .Where(x => x.mj.Id == x.mn.MajorCategoryId)
  .Select(x => new { Major = x.mj, Minor = x.mn });
 
foreach (var x in q)
{
  var line = string.Join(", ", x.Major.Name, x.Minor.Name);
  Console.WriteLine(line);
}
List 16 SelectManyメソッドを使って2重ループ相当のLINQクエリを書く例

 しかし、この書き方には少々慣れが必要で、すぐに書ける人は少ないらしい。「らしい」と書いたが、実際、C# 3.0をリリースするに当たってユーザビリティ・テストを行ったところ、「書ける人がほとんどいない」という結果が得られたといわれている。

 たまに「関数型脳が必要」などと例えられることもあるように、SelectManyのような書き方も関数型言語では一般的なのだが、C#ではやはり、List 15の2重ループからの類推が求められるようだ。

 そこで導入されたのがクエリ式(query expression)である。List 17に示すような、「SQL風の式」だ。

var q =
  from mj in data.MajorCategories
  from mn in data.MinorCategories
  where mj.Id == mn.MajorCategoryId
  select new { Major = mj, Minor = mn };
List 17 クエリ式を使って2重ループ相当のLINQクエリを書く例
2つ目のfrom句はSelectManyメソッドに展開される。

 foreach文のところをfrom句に、if文のところをwhere句に、「yield return」を書くべきところをselect句に置き換えればよい。これで、List 16のようなメソッド呼び出しに変換される。

 クエリ式はいわば、命令型言語と関数型言語の橋渡しといっていいだろう。end of article

 

 INDEX
  特集:Road to LINQ
  C#で解説する「データ処理の直交化と汎用化」
    1.直交性
    2.LINQ to Objectに至るまで
  3.LINQ to ……/クエリ式


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)
- PR -

注目のテーマ

業務アプリInsider 記事ランキング

本日 月間
ソリューションFLASH