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 でも 論理的には 、 Select
は SelectMany
の特別な呼び出し方の1つに過ぎない。もしもC#にもっと効率のよい機構がなかったとするなら、この実装を使ってもいい。 シーケンスを受け取って新しいシーケンスを返すクエリ内包であれば、どんなものでも全部同じことが言える; SelectMany
があるからこそ、モナド的ワークフローの末尾に任意の処理を結合し、新しいワークフローを返すことができるのだ。
次回のFAIC では、モナドに対するバインド操作と「クエリー内包」の文法との関連性を見ていく。
-
selectクエリーの方が基本的なクエリーなんじゃないかと思うかもしれないが、後でわかるように、selectはselect manyの特殊例の1つでしかない。前にも
ApplyFunction
はApplySpecialFunction
の特殊例の1つでしかないと言った。この関係性は偶然の一致ではない!Select
とは、シーケンスモナドのApplyFunction
なのだ。 ↩︎ -
解説をよりわかりやすくするため、エラー処理を除いている。 ↩︎
-
そしてもちろん、
CreateSimpleSequence
もこっそり使っている。さてどこでしょう? ↩︎ -
そして
Where
、Select
、SelectMany
があれば、Join
も実装できる。とても効率の悪いJoin
にはなるだろうけど、作ることは可能だ。これは練習問題として残しておく。 ↩︎