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

なぜ C# でメソッドチェインが楽しいのかを考えてみた

.NET

未だにモニャド (失礼,かみました) が何なのかよく分かってないんですが,何となく分かってきたような気もするので,とりあえず何か書いてみる試み.とはいえよく分かっていないかもしれない言葉で書くのは怖いので,以下では C# の言葉で何が楽しいのかを書いてみることにしますよ.

LINQ における interface の使い方は今までとちょっと違う

Java 以来 (もっとも,私にとってこれは COM 以来の,ですが) の「interface の時代」にプログラミングするときに,私の関心は主に次の 2 点に集約されていました.

  • いま考えているこのクラスはどんな interface を実装するか?
  • いま考えているこの処理を行うためには,どんなオブジェクトを受け取って,それはどんな interface を備えていれば必要十分か?

例えば ArrayList は IList だよねとか,このアルゴリズムでソートするなら IList で取りたいよねとかその辺です.
どんな interface を定義すべきかは,だいたいこの 2 つの関心のバランスを取ることで行ってきました.頻繁に行う処理に必要な性質を interface として定義すれば,処理を書くときにはより多くのオブジェクトに適用できてハッピーです.あまりにもマイナーな性質を interface として定義しても,その interaface は特定の型専用というものになってしまいます.もっとも,外部に公開されている性質は全て interaface だと考えるのも,それはそれで有用でした.それを私は COM と DI から学びました.
では,LINQ における interface の使い方は何なのでしょうか?
「これは今までとちょっと違う」ということを強調しておきたいと思います.ご存じのように,LINQ では,IEnumerable<T> を受け取って,IEnumerable<U> を返す処理の連鎖として処理を記述します*1.T や U には特に制限を設けていないので,ここにはどんな型でも入れられます.T と U は同じ型でも異なる型でも構いません.つまり,

  • 処理の前後で IEnumerable<(何か)> という大枠は変わらない
  • (何か) の型は処理の前後で変化してよい

というルールです.たったこれだけのルールでも,Extension Methods と組み合わせればどれだけ面白いことができるかは,皆さんもうだいぶご覧になったことでしょう.

なぜ LINQ の基本は IEnumerable<T> であって IList<T> ではないのか?

これは簡単で,IList<T> で作ると窮屈になってしまうからです.
先ほどのルールを一般化してみましょう.

  • 処理の前後で Interface<(何か)> という大枠は変わらない
  • (何か) の型は処理の前後で変化してよい

ここで Interface に IList を当てはめてみます.
IList <T> 連鎖というルールに従う世界には,IList <T> から IList<U> を作り出す処理のみが参加できます.しかし,IList <T> 連鎖の世界の多様性は,IEnumerable<T> から IEnumerable<U> を作り出す処理の世界の多様性に比べれば幾分 (人によっては大幅に) 制限されたものになるでしょう.例えば遅延クエリが使えなくなってしまうので,LINQ to SQL への応用は非常に困難になります.つまり,そんな世界は「今ひとつ面白くない」のです.確かに特殊な場面では役に立つかもしれませんが,あえて標準ライブラリ化するほどのものでもないというわけです.
IEnumerable<T> という interface は,それ以上シンプルにすると連鎖構造のメリットが失われはじめるというギリギリのラインまで攻めのシンプル化を行っています.そのおかげで,IEnumerable<T> 連鎖の世界に参加できる処理は多種多様になりました.気の利く処理に小粋な処理,取っつきにくいけど使い慣れたらとても便利な処理まで,実に様々な処理が参加することができるのです*2

従来視点だと IEnumerable<T> は単にオブジェクトコンテナに共通する性質を抽象化しているだけのように見えるかもしれませんが,連鎖できる処理の構造化という視点で眺め直すと,IEnumerable<T> が LINQ ワールドの基盤であることが見えてくるのですよ.
そして,メソッドチェインの極意もここにあるように思います.それは,

  • Interface<X> を受け取って Interface<Y> を返すような手続きのうち,それを連鎖的に行って意味があるInterface<T> を見いだし,それをできるだけシンプルにする.
  • ある手続きが,Interface<X> から Interface<Y> への変換として定義できないかを考える

というものです.

Extension Methods とメソッド連鎖と IntelliSense

先ほどは 「処理の前後で Interface<(何か)> という大枠は変わらない」と書きましたが,この Interface と書いた部分を generic class や generic struct に置き換えても議論は成り立ちます.実際前回書いた Maybe は generic struct ですしね.以下,大文字で始まる Interface と書いたときは,generic class や generic struct に置き換え可能なものと考えて下さい.
また,(何か)の部分が常に同じ型の場合も特殊系として同じ議論で考えることができます.StringBuilder のメソッドチェインなどはまさにそんな感じでしょう.
さて,「Interface<T> を受け取って,Interface<U> を返す」一連の流れを,C# 3.0 のシンタックスとして左から右に,上から下に連鎖させることが可能なのは,実際 LINQ を見れば明らかでしょう.「受け取っての部分」は,Extension Methods で簡単に解決できます.そして戻り値は「意図的に」 Interface<(何か)> に固定しているのですから,これに対しても同じようにインスタンスメソッド形式の呼び出しが可能なのは当然です.偶然ではありません.そうなるように意図的に設計されているのです.
完成したメソッドチェインは,IntelliSense のおかげで非常に気持ちよく使えます.一度 Interface<(何か)> の連鎖モードに入れば,その連鎖の中で可能な処理のみが*3 IntelliSense に表示されます.
これって何というか,言語内の“ミニモード”って感じですよね? あるいは Joel の言うところの「Zone に入る」のニュアンスで,“ミニゾーン”.

そもそもなぜメソッドチェインで書くのか?

メソッドチェインは,シンプルな処理の合成で大きな計算を組み立てるのに向いています.そのメソッドチェインの世界で有用と思われる処理は,先人達が既に整備してくれています.IntelliSense は,そんな実装済みの処理一覧を提示してくれます.
“ミニモード”に入ったときのあなたの仕事は,IntelliSense の候補一覧 (といってもそれは十分短いものです) の中から次にやるべきことを選び,それを連結していくことです.そうすれば,自然に複雑な処理が記述できるのです.なにがすごいって,提示される処理は,どれも言語組み込みキーワードと同じぐらい,その“ミニモード”では本質的な操作なのですから.
"One more thing!"
そのリストはオープンです! とびきりお気に入りの関数を,あなたは自由に追加することができるのです!

ちょっとだけ例をお見せしましょう.

List<string> lines = new List<string>();
using (StreamReader sw = new StreamReader("hoge.txt"))
{
    while (true)
    {
        string line = sw.ReadLine();
        if (line == null) break;
        lines.Add(line);
    }
}

これは,従来の制御構造を使った C# のコードです.これを LINQ to Object という“ミニモード”を使ったコードに書き換えます.

var strategy = Make.Repeat(() => new StreamReader("hoge.txt"))
                   .Select(sr => sr.ReadLine())
                   .TakeWhile(sr => sr != null);

var lines = strategy.ToList();

一番上の Make.Repeat は,私が作ったメソッドで,using(T = ...) while(true) という構造を IEnumerable<T> に変換する魔法の呪文です.あとは「ゾーンに入って」えいやっと書いちゃいましょう.気分はカードゲームで.
Make.Repeat のおかげで,ターンの最初に StreamReader のカードが常に配られてきます.
StreamReader から文字列を取り出すのは,StreamReader のカードを捨てて文字列のカードを引く感じ.これは Select です.
if (line == null) break; は,引いたカードが null だったら「ストップ」という感じ.ここは TakeWhile です.
これでゲームの戦略は固まりました.
あとはそれを実際やってみて,得られた手札を全て箱に入れて取っておく感じに ToList.これは元のコードが List<T> に結果を残していたからやってるだけですけどね.
戦略の立案と実行を一気に行うなら,strategy という変数は消去して,全部一気に書いてしまって構いません.これができるのもメソッドチェインの便利なところなのですから.

var lines = Make.Repeat(() => new StreamReader("hoge.txt"))
                .Select(sr => sr.ReadLine())
                .TakeWhile(sr => sr != null)
                .ToList();

まとめ

最後にポイントをまとめておきましょう.

  • generics は「全ての型を横断する何かのグループ」を定義するのに使える.
    • IEnumerable<T> と IEnumerable<U> は「ある意味で」同じグループに属するとみなせる.T と U はなんでもよい.
  • Extension Methodsを使えば, 「型を横断する何かのグループ」の中で共通に使える基本ロジックを定義できる.
    • しかもそれはインスタンスメソッドのように呼べる.
    • そのロジックが「型を横断する何かのグループ」の中で閉じていれば,必然的にチェインできる.
  • 「型を横断する何かのパターン」と,頻出パターンのユーティリティ関数を有機的に結びつけたとき,IntelliSense 効きまくりのミニモードができあがる

というわけで,皆さんも Let's chain!

おまけ

2 年まえに id:ladybug さんに紹介していただいた Perl6 の新機能,パイプ演算子

というのをみて、そういえば Perl6 では代入に関する演算子が追加されるようだ、と。

$a = 1 + 1;
$a <== 1 + 1;
1 + 1 ==> $a

2つめと3つめが新しい演算子。見た目とおりの機能を提供する。

dataset1 ==> where(<category> == targetCategory)
         ==> select(<name>, <price>)
         ==> calc(<price> *= 0.05)
         ==> sum(<name>)
         ==> printf("%s %d\n", <name>, <price>)

みたいなかんじで、処理を順序だてて記述することができる。遅延評価もあるので上記は全部遅延評価される・・・かもしれない。

確かに今なら実感をもって理解できます.

おまけ2 (2008年7月31日追記)

クエリ式の into,まさに左から右への変数導入・束縛ですな.

C# 言語リファレンス

into (C# リファレンス)

into コンテキスト キーワードを使用すると、group、join、または select 句の結果を新しい識別子に格納するための一時識別子を作成できます。この識別子自体が、追加のクエリ コマンドのジェネレータになります。group 句または select 句で使用する場合、この新しい識別子の用法は、継続と呼ばれることがあります。

(中略)

// Create a data source.
string[] words = { "apples", "blueberries", "oranges", "bananas", "apricots"};

// Create the query.
var wordGroups1 =
    from w in words
    group w by w[0] into fruitGroup
    where fruitGroup.Count() >= 2
    select new { FirstLetter = fruitGroup.Key, Words = fruitGroup.Count() };

*1:意味を考えるなら,連鎖させる処理として結合則が成り立つものを考えるべき,と monad 則も言っている気がしますが,とりあえず今回の話では SelectMany を使わないので,シンタックス上は後ろに処理をくっつけていく話しかでてきません.なので,そもそも結合則が成り立たないような連鎖はまともな連鎖として認識されないだろうということにして,結合則については深入りしないことにしました.

*2:ちなみに,LINQ の世界での IEnumerable<T> から IEnumerable<U> を作り出す処理は全て SelectMany を使って書くことができます.これは理由があって,連鎖の各段が SelectMany で書けることを「LINQ の世界における連鎖構造の条件」と定義したためです.つまり,「各段が SelectMany を使って書くことができる」という条件を使って「連鎖できること」を定義していたわけで,この性質は当たり前のことを言っているにすぎないのですね.←なんかやっぱりこれは間違ってる気もしてきた.続きは Web で.

*3:実際には,ToString や GetType などのメソッドも若干含まれる.