C#で部分適用
(話とかどうでもいいからソースみたい人は→Gist)
C#で部分適用をしようと思ったら、普通は
_1 => _2 => foo("hoge", _1, _2)
のようにラムダ式を書かなければなりません。
しかし、これが例えばScalaだと
foo("hoge", _, _)
と、適用しない(引数になる)ところに_
を置くことで部分適用された関数を作ることが出来ます。
はい。うらやましいですよね。妬ましいですよね。
これをC#でどうにかやってやろうじゃないのって思いますよね。 では、やりましょう。
方法
まず今やりたいことは、Func<>
を拡張したいということなので、拡張メソッドを使うことはすぐ決まります。
そして、部分適用をする上で最低限必要な情報はどこが適用されないかです。
なので単純な方法として、適用しないところにそれを表す値を渡すことが考えられます。
例えば、Option<T>
型のようなものを取るようにして、None
が来たところは_
にあたるみたいな感じで行けそうに思えます。
しかし、これには問題が有ります。
部分適用した関数の型は、適用しなかった部分の型を取る関数になります。
例えば最初にあげたfoo
関数がFunc<string,int,double>
型ならば、
foo("hoge", _, _)
はFunc<int,double>
型になるわけです。
しかし今考えた値レベルで適用しない部分を指定する方法では、型をつけることが出来なくなってしまいます。(依存型でもあれば別だが)
つまり、素直に部分適用後の関数に適切に型を付けるためには、適用しない部分が型レベルにわかる必要がありそうです。
これはオーバーロードでどうにか出来そうに思えます。
例えば、適用しない部分をPlaceholder
型として2引数関数を対象とすると、
public static Func<T1,TResult> PartialApply<T1,T2,TResult>( this Func<T1,T2,TResult> func, Placeholder p, T2 v2) { return _ => func(_,v2); } public static Func<T2,TResult> PartialApply<T1,T2,TResult>( this Func<T1,T2,TResult> func, T1 v1, Placeholder p) { return _ => func(v1,_); } public static Func<T1,T2,TResult> PartialApply<T1,T2,TResult>( this Func<T1,T2,TResult> func, Placeholder p, Placeholder p) { return (_1,_2) => func(_1,_2); }
みたいな感じでしょうか。 手書きは辛いでしょうがそこはT4にでも頼めば何とかなりそうです。
でもなんか嫌な予感がします。
これが3引数になったらどうなるでしょうか。各引数で適用する/しないで2通りあるので、全引数に適用する場合を除いたとして23-1 = 7通り。
もう少し行きましょう。4引数、24-1 = 15通り。5引数、25-1 = 31通り。…8引数、28-1 = 255通り。…
はい、もうお分かりの通り引数のパターンは指数的に増加していきます。255通りならまだ何とかなりそうですが、Func<>
型は16引数まで標準であるのでそこまで対応しようとすると、216-1 =65535通りにもなり、とてもやばいです。
というわけで、この方法ではどうにもうまくいきそうもないです。
さてどうしましょう。
これが第1引数だけ考えればいいのであれば楽なのですが…
第1引数のみ…多引数関数が第1引数…
(´・◞◟・)なんかカリー化したらうまくいきそうじゃね?
はい、なんかそんな気がしたのでカリー化した状態で今一度部分適用を考えてみます。
まずは2引数の場合
2引数関数をカリー化するとFunc<T1,Func<T2,TResult>>
のようになります。
ここで普通に第1引数を適用したいなら、func(v1)
のようにすればいいですね。そして、第1引数を適用しないなら、次に第2引数を与えると、部分適用された関数、つまり第1引数の型を取る関数になってくれればいいので、この関数を拡張メソッドで_
として定義すると…
public static Func<T1,Func<T_,TResult>> _<T_,T1,TResult>( this Func<T_,Func<T1,TResult>> func) { return v1 => _ => func(_)(v1); }
ですね。(上のコードではT1がT_、T2がT1になってます)
さらに3引数では…
public static Func<T1,Func<T2, Func<T_, TResult>>> _<T_, T1, T2, TResult>( this Func<T_, Func<T1,Func<T2, TResult>>> func) { return v1 => v2 => _ => func(_)(v1)(v2); }
です。
これは結局のところ、カリー化してあれば、適用しない場合は第1引数を一番後ろに回せばいいということです。
この場合だと、各引数の数に対して、必要なメソッドはたった1つです!
組み合わせ爆発を克服出来ました!
これを使うためにはカリー化しなければならず、部分適用した後もカリー化された状態になってるので、他の関数と同じように扱うにはアンカリー化する必要がありますが、カリー化/アンカリー化も各引数の数に対して1つで済みます! やった!
また、見た目も
foo.Curry()("hoge")._()._().Uncurry()
のような感じになり、ギリ分かるレベルでしょう。
最後に、この方針で、軽く実装してみました。
ソースコードがGistにあります。
また実行した結果をIdeoneにあげておきました。
手動で書いたので4引数までしかないですが、後でT4で16引数まで伸ばす予定です。