関数合成

LangExtでは関数合成のために、Chainというメソッドを提供しています。 ここでは、そのほかの方法ではなく、Chainのみを提供した理由を説明します。

関数合成とは

関数fとgがあった時、fの戻り値の型とgの引数の型が一致した場合に限って、この2つの関数を合成することが出来ます。 例えば、stringを受け取ってDateTimeを返す関数fと、DateTimeを受け取ってintを返す関数gがあった場合、 LangExtでは以下のように2つを合成した関数hを作れます。

var h = Func.Chain(f, g); // string -> int

Func.Chainは拡張メソッドになっているため、fがFuncデリゲートの場合は

var h = f.Chain(g);

と書くこともできます。

関数合成の実装方法

関数合成の実装には、様々な方法が考えられます。

演算子のオーバーロード

まず、演算子のオーバーロードが考えられます。 しかし、C#ではデリゲートに対して演算子を定義することが出来ないため、 演算子のオーバーロードを使う場合、Funクラスなどの独自型を作る必要が出てきます。

また、classは変位指定(in/out)ができないので、Funcデリゲートよりもある点では劣ったものになってしまいます。

LangExtでFunのようなクラスを作った場合、それに付随して生成しなければならないものも多くなるため、 この方針はLangExtに適していないと判断し、却下しました。

Compose

次に、Composeというメソッドを使う、という案です。 Haskellで関数合成と言うと、f(g x)のfとgをf.gと合成することです。 LangExtのChainとはfとgの関係が逆転していることに注意してください。 LangExtでは、g.Chain(f)となります。

Scalaでは、この関数合成をcomposeというメソッドとして提供しています。 これをそのまま採用し、Composeというメソッドを実装してもよかったのですが、 この方式では関数の合成を連続で行おうとした場合に、Composeの呼び出しをネストする必要があります。

var f = Func.Compose(f4, Func.Compose(f3, Func.Compose(f2, f1)));

これは非常に面倒なうえに分かりにくいので、却下しました。

AndThen

ScalaはandThenという名前でcomposeでのfとgを入れ替えたメソッドも提供しています。 これを採用すると、上の例はわかりやすく書けるようになります。

var f = f1.AndThen(f2).AndThen(f3).AndThen(f4);

大分改善されましたが、いちいちAndThenと書くのが面倒です。

Chain

上記のAndThenでもよかったのですが、T4 Templateによる生成を活用することでさらに便利な記述が可能になります。

var f = Func.AndThen(f1, f2, f3, f4);

これは、可変長引数では実現できません。 なぜなら、f1の戻り値の型とf2の引数の型、f2の戻り値の型とf3の引数の型、f3の戻り値の型とf4の引数の型が、 それぞれ一致している必要があるからです。 可変長引数で実現しようとした場合、全ての関数の引数の型と戻り値の型が一致している必要があり、 使い道のない関数になってしまいます。

これをT4 Templateを使って「前の関数の戻り値の型と同じ引数の型を持つ関数しか渡せない」ことを実現しています。 擬似的に可変長引数を実現したため、AndThenではちょっと意味が通らなくなってしまいました。 そこで、これを「関数の連鎖」ととらえ、Chainという名前を使うようにしました。

T4 Templateで生成している関係上、16引数までしか対応していませんが、 それ以上の個数の関数を合成したい場合は、Chainが拡張メソッドであることを利用します。

var f = Func.Chain(f1, f2, ..., f16).Chain(f17, f18, ...

ここまで大量の関数を合成する場合、設計を疑った方がいいかもしれませんが。

Func.Chain形式と拡張メソッド形式の使い分け

Chainは拡張メソッドとして実装されているため、f.Chain(g)Func.Chain(f, g)という2通りの書き方ができます。 これらの使い分けは、最初の関数がFuncデリゲートかどうかと、合成する関数が1つかどうかで判断します。

最初の関数がFuncデリゲートではない場合、拡張メソッド形式はそのままでは使えません。

var f = DateTime.Parse.Chain(...); // コンパイルエラー

これは、Func.ChainがFuncデリゲートの拡張メソッドとしてしか定義されておらず、 DateTime.Parseは単なるstaticメソッドでありFuncデリゲートではないのが原因です。 これをいちいちFuncデリゲートに変換するのは面倒なので、 最初の関数がFuncデリゲートではない場合は、Func.Chain形式を使うのがいいでしょう。

var f = Func.Chain(DateTime.Parse, ...);

最初の関数がFuncデリゲートかつ、 合成する関数が1つ(つまり2つの関数を合成する)場合は、拡張メソッド形式を使います。

var h = f.Chain(g);

合成する関数が2つよりも多い場合は、Func.Chain形式を使います。 これは、単に分かりやすさのためです。

var f = Func.Chain(f1, f2, f3);

ただし、合成したい関数が16を超える場合は、拡張メソッド形式で繋ぐ必要があります。

また、合成する関数をその場でラムダ式として書く場合で、その中身が長くなる場合は、 拡張メソッド形式で記述した方が分かりやすくなるかもしれません。