matarillo.com

The best days are ahead of us.

FAIC モナド、パート10

2018-01-19 17:14:18

原文

前回のFAICではシンプルな「ある値に別のデータを関連付ける」モナドの例をいくつか提示した。これらも便利なんだけれど、モナド・パターンの真の力は、データに対するワークフローを表現するオブジェクトを生成する時に発揮される。C#でのいちばんいい例はシーケンスモナド IEnumerable<T> だ。実のところ、LINQの最重要ポイントはとあるモナド的ワークフローを簡単に生成できることで、そのワークフローとは「クエリー」のことだ。

何度か言ったことがある話だけど、もしLINQ入門者にたった1つだけLINQの説明をできるとするなら、こう言うだろう: クエリーオブジェクトは クエリー を表現しているんであって、 クエリーの実行結果 じゃない、と。この件に関する最も基本的な例を見てみよう。「select many」クエリーだ。1 LINQ to Objectのオーバーロードされた SelectMany メソッドのうちの1つの実装がこれだ:2

static IEnumerable<R> SelectMany<A, R>(
  this IEnumerable<A> sequence, 
  Func<A, IEnumerable<R>> function) 
{ 
  foreach(A outerItem in sequence) 
    foreach(R innerItem in function(outerItem)) 
      yield return innerItem; 
}

すぐ気付いたと思うが、このシグネチャーはここ5、6回ほどの連載の中に登場している: SelectMany とは、シーケンスモナドの Bind 、またの名をApplySpecialFunction だ。そして、 Bind って何をするんだったっけ? このメソッドはあるワークフロー(つまりあるシーケンス)とある関数を受け取り、ワークフローの出力に対して関数を論理的に「バインド」する新しいワークフローを生成する。実例を見ながら考えてみよう:

static IEnumerable<int> Odd(int i) 
{ 
  if (i % 2 != 0) 
    yield return i; 
}

そして、整数のシーケンスを生成するワークフローがあったとしよう:

IEnumerable<int> original = whatever;
IEnumerable<int> query = original.SelectMany(Odd);

結果はどうなった?「※ただし偶数に限る」という操作をワークフローの最後にくっつけることで、古いクエリーから新しいクエリーを生成した。この例を一般化することもできる:

static IEnumerable<T> WhereHelper<T>(
  T item, 
  Func<T, bool> predicate) 
{ 
  if (predicate(item)) 
    yield return item; 
}
...
Func<int, IEnumerable<int>> odd = num => WhereHelper<int>(
  num,
  item => item % 2 != 0); 
IEnumerable<int> query = original.SelectMany(odd);

でもこれではちょっと使いにくすぎる。まるっとヘルパーメソッドで抽象化した方がいい:

static IEnumerable<T> Where<T>(
  this IEnumerable<T> items, 
  Func<T, bool> predicate) 
{ 
  return items.SelectMany(item => WhereHelper(item, predicate)); 
}
...
IEnumerable<int> query = original.Where(num => num % 2 != 0);

何を作ったかお分かりかな?まさに今、SelectMany といくつかのヘルパーメソッドを呼び出すだけで Where メソッドを実装したんだ。3 とはいえこうやって Where を実装するのはアホほど遠回りではある。C#の機能を利用すれば、もっと短くもっと効率的に実装できる。ただし言いたかったのは、Where論理的には 単なるSelectMany の呼び出しに毛が生えただけのもので、やっていることは同じだということだ: すでにあるワークフローの末尾に新しい処理を結合するだけ。もちろんこれはSelect でも同じことが言えて、全体を SelectMany で書き換えることができる:

static IEnumerable<R> SelectHelper<A, R>(
  A item, 
  Func<A, R> projection) 
{ 
    yield return projection(item); 
}
static IEnumerable<R> Select<A, R>(
  this IEnumerable<A> items, 
  Func<A, R> projection) 
{ 
  return items.SelectMany(item => SelectHelper(item, projection)); 
}

...

IEnumerable<int> query = original.Select(num => num + 100);

繰り返すが、分別ある人間は実際には Select メソッドをこんな風に実装したりはしない。C#ではもっと短くてもっと効率的な実装ができるからだ。4 でも 論理的にはSelectSelectMany の特別な呼び出し方の1つに過ぎない。もしもC#にもっと効率のよい機構がなかったとするなら、この実装を使ってもいい。 シーケンスを受け取って新しいシーケンスを返すクエリ内包であれば、どんなものでも全部同じことが言える; SelectMany があるからこそ、モナド的ワークフローの末尾に任意の処理を結合し、新しいワークフローを返すことができるのだ。

次回のFAIC では、モナドに対するバインド操作と「クエリー内包」の文法との関連性を見ていく。


インデックスへ戻る


  1. selectクエリーの方が基本的なクエリーなんじゃないかと思うかもしれないが、後でわかるように、selectはselect manyの特殊例の1つでしかない。前にもApplyFunctionApplySpecialFunction の特殊例の1つでしかないと言った。この関係性は偶然の一致ではない!Select とは、シーケンスモナドの ApplyFunction なのだ。 ↩︎

  2. 解説をよりわかりやすくするため、エラー処理を除いている。 ↩︎

  3. そしてもちろん、CreateSimpleSequence もこっそり使っている。さてどこでしょう? ↩︎

  4. そして WhereSelectSelectMany があれば、Join も実装できる。とても効率の悪い Join にはなるだろうけど、作ることは可能だ。これは練習問題として残しておく。 ↩︎