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

アップキャストと型推論に見る思想の違い

.NET

C++ でメソッドチェインが流行らなかったのは参照の寿命管理が厄介だったからじゃね?

いやまあ適当に言ってみただけです気にしないで下さい.実は単に偶然の産物という気もします.

struct Odd
{
    int dummy;
};
struct Even
{
    int dummy;
};
Odd ToOdd(Even& source)
{
    return Odd();
};
Even ToEven(Odd& source)
{
    return Even();
};

Even operator >> (Odd& source, Even op(Odd &) )
{
    // 実用上はコピーなしに一時オブジェクトの参照を返したい
    // C++0x では解決かな?
    return op(source);
}

Odd operator >> (Even& source, Odd op(Even &) )
{
    return op(source);
}

int main()
{
    Odd odd;
    Even even = odd >> ToEven
                    >> ToOdd
                    >> ToEven
                    >> ToOdd
                    >> ToEven;
    return 0;
}

Haskell の >>= とか >>,F# の |>,JavaScriptRuby のメソッドチェイン,C# の Extension Methods を使ってみて思ったのが,チェイン時にレシーバの型と戻り値の型を変えられるのは実はとてもありがたいということです.
動的型付け言語である JavaScriptRuby であれば,元から戻り値の型は実行時にころころ変えられるので,何を今更,な話でしょう.
じゃあ静的型付け言語である Haskell,F#,C# ではどうかというと,パイプライン後段のメソッドルックアップが,パイプライン前段のメソッドルックアップに依存しているというのがとても便利です.パイプライン前段のメソッドルックアップをちょっと変えてやると,そこよりあとの段についてはコンパイラが最適なメソッドを選び直してくれるのです.LINQ to Object のソースコードが,「より優先度の高いルックアップルールを using で導入するだけで」,LINQ to SQL になるってのはそういうことですよね.

ここまで読んだとローカル変数の型推論

パイプライン形式で気持ちよくかけているときには,処理を一回書くたびにいちいち「最も抽象性が高い型を考えてそれにアップキャスト」なんてしたくなくなります.基本的にメソッド連鎖が適切な抽象性の型を選んでくれます.だったらコンパイラに任せれば良いだろうと.
また,切りがいいから一旦変数に代入/束縛するよという場合でも,私はいちいち型を書きたくありません.そこに型を書いてしまうと,代入/束縛という単純な意味以外に,元のパイプラインには存在しなかった型変換という別の作用を含んでしまうことになります.

int main()
{
    Odd odd;

    auto query = odd >> ToEven
                     >> ToOdd
                     >> ToEven; // ←ここまで読んだ

    Even finalResult = query >> ToOdd
                             >> ToEven;
    return 0;
}

上の「ここまで読んだ」のところの型が,Even なのか SpecializedEven なのか書かない方がむしろ良くね? というわけです.特定の型を強制したければ,CastTo<T> という処理でも作って明示すればよいでしょう.
変数代入に紛れてこっそりアップキャストされると,メソッドルックアップルールを活用したメソッド選択/差し替えの結果が変わってしまいます.

ところ変わればプログラマの意図も変わる

以上のようなことを思ったのは実は昨年暮れだったりするのですが,あとはまあ『ベイダー日記 2008-02-21』を読んでまた漠然と思い出したり,最近流行のローカル変数のアップキャスト議論で思い出したり.

わかりきったとこならvarでもいぃんとちゃうの?
これについても同意っちゃー同意なんですけど、
僕がこだわるのがコンパイラ型推論
プログラマの意図と一致しないことがあるなー、と。

var children = new List<Child>();

僕はおそらく、このテの(省略目的の)varは使わない気がします。
childrenはList<T>でなくてはならないとは思っていない、
Childの集合でありさえすればえぇのや、

ICollection<Child> children = new List<Child>();

なのよね僕のキモチ的には。

この辺は仮名漢字変換を単文節でぶつ切り変換するのが好きか,長文で一気に変換するのが好きかみたいなもので,もはや好みの問題かなぁという気もします.私も昔はぶつ切り変換的にコーディングしていましたが,最近は逆です.型を介してシンプルなロジックを連結するスタイルに変わりました.シンプルなロジックの連結だけを書いておけば,最後にコンパイラが型を介して一気に巨大なロジックを組み立ててくれるよ教に宗旨替えです.
半端なローカル変数や private メソッドを排すことで,思考速度とロジックの結合速度が一致してくる感覚は楽しいですね.「この流れで行けばうまく組み立て挙げられる」と確信しているときに,いちいち型を書いたりしないですよと.型のミスマッチはコンパイル時に検証されるので,意図とコードがズレていないかのみが重要です.その意味では,ぶつ切り変換的なコーディングも,込めている意図があってそれとはズレていないよというだけの話なんでしょうけどね.スタイルの違いなので,まあ好きな方を使えばよいのでしょう.
私はというと,長文変換的なコーディングをするようになって,関数テンプレートの型推論*1やラムダ式の型推論,メソッドオーバーロードのルックアップがいかに重要かというのが納得できました.これに関しては,実際に自分でやってみるまで,いくら文章で説明されても実感が伴った理解になっていなかったと断言できます.
今の自分が過去の自分に「var や let で定義した変数は,型変換という副作用を持たないが故に,処理と処理の連結器のように使うことができるのです」なんて言ったとしても,今の自分が意図しているところはなかなか伝わらないんですよね,多分.とまあ多分にキモチ的な問題なんだと思います.

*1:C++ 屋さんにとっての,きれいな型推論 or よい型推論