Java 8を関数型っぽく使うためのおまじないをC#でやってみた
Java 8を関数型っぽく使うためのおまじない - きしだのはてな
Java は大変ですね。 C# さんは、ラムダ式も Func 型(Java の Function 型に大体対応)も Visual Studio 2008 時代(5年前)から使えたので、似たようなことはすでにできます。 上記の記事のパクリなので、上記の記事をまずは読んでから読むことをおすすめします。
Func 型
C# では Func デリゲートというものがあります。名前空間名や型パラメータまで含めると、System.Func<TArg, TResult>
です。
こんな感じで使います。
Func<string, string> enclose = s => "[" + s + "]";
これを呼び出そうとすると、こんな感じになります。
// Javaだとenclose.apply("foo")と、applyが必要 System.Console.WriteLine(enclose("foo"));
こうすると、次のような表示になります。
[foo]
もうひとつ関数を定義してみましょう。最初の文字を大文字にする関数です。
Func<string, string> capitalize = s => s.Substring(0, 1).ToUpper() + s.Substring(1).ToLower();
2文字未満の文字列を与えると死にます。
呼び出してみます。
// applyは不要 System.Console.WriteLine(capitalize("foo")); // => Foo
この2つを順に呼び出して、capitalize して enclose しようとすると、こんな感じになりますね。
System.Console.WriteLine(enclose(capitalize("foo"))); // => [Foo]
こういう場合、Java では andThen を使って連結できるみたいですが、C# にはそんなものはありません。 拡張メソッドで追加しましょう。
using System; public static class Func { public static Func<T, V> AndThen<T, U, V>(this Func<T, U> f, Func<U, V> g) { return t => g(f(t)); } }
使ってみましょう。
System.Console.WriteLine(capitalize.AndThen(enclose)("foo"));
これは、関数合成ですね。
Func<string, string> capEnc = capitalize.AndThen(enclose); System.Console.WriteLine(capEnc("foo"));
C# でも関数型っぽいことができることがわかりました。やったね!
別名を使う?
Func はそんなに長くなくてそれなりにいい感じだと思います。 あと、Func オブジェクトをすでに持っているのであれば、var が使えます。
// これは怒られちゃう // var enclose = s => "[" + s + "]"; Func<string, string> enclose = s => "[" + s + "]"; Func<string, string> capitalize = s => s.Substring(0, 1).ToUpper() + s.Substring(1).ToLower(); // Funcオブジェクトに対する操作の結果であれば、引数の型も戻り値の型も不要! var capEnc = capitalize.AndThen(enclose);
Action
戻り値を返さない関数は、C# では Func が使えません(Func<string, void>
がエラー)。
仕方ないので Action を使います。
Action を使うと、System.Console.WriteLine も扱えます。
Action<string> writeLine = System.Console.WriteLine; var enclose = (string s) => "[" + s + "]"; writeLine(enclose("foo")); var capitalize = (string s) => s.Substring(0, 1).ToUpper() + s.Substring(1).ToLower(); writeLine(capitalize("foo"));
ここで、1引数の Func しか使ってませんが、C# は型パラメータの数が違えば同じ名前で型が定義できます。 それを利用して、標準で 16 引数まで対応した Func/Action が用意されています。 が、2引数以上の関数は甘えなので、1引数の Func/Action さえ定義されていれば問題ありません。
関数合成
関数合成は次のように書けます。
writeLine(capitalize.AndThen(enclose)("foo"));
さらにもうひとつ関数を用意して、次のように書いてみます。真ん中あたりを取り出す関数です。
// Substringの第二引数の意味が、Javaとは違うので注意! var middle = (string s) => s.Substring(s.Length / 3, s.Length - s.Length * 2 / 3); writeLine(middle.AndThen(capitalize).AndThen(enclose)("yoofoobar"));
関数合成を使わないと、次のようになりますね。
writeLine(enclose(capitalize(middle("foobaryah"))));
このように、実際に呼び出す順と記述が逆になります。middle して capitalize して enclose するのに、先に enclose から書く必要があります。 また、カッコがたまっていくので、最後にまとめてカッコを閉じる必要があります。ちょっと関数呼び出しが増えると、閉じカッコの数が分からなくなってコンパイルエラーがなくなるところまで「)」を増やしたり減らしたりなんてこと、ありますよね?
元記事には日本語名のメソッド定義してますが、省略します。
カリー化形式
さて、2引数以上の関数は甘えと書きましたが、実際2つ以上のパラメータを渡したいときはどうすればいいんでしょう? こういうときに使うのがカリー化です。カリー化は、ひとつの引数を取って関数を返すことで、複数のパラメータに対応します。
例えば、はさむ文字列とはさまれる文字列を指定して、文字列をはさむ関数は、通常の2引数関数であらわすならこうなるでしょう。
string Sandwich(string tag, string str) { return tag + str + tag; }
これをカリー化形式の関数で書く(もしくは、1引数関数のみを使って書く)と、次のようになります。
Func<string, Func<string, string>> sandwich = tag => str => tag + str + tag;
sandwich 自体は、『文字列を取って、「文字列を取って文字列を返す関数」を返す関数』になっています。
呼び出しは次のようになります。
writeLine(sandwich("***")("sanded!")); // => ***sanded!***
3引数だとこんな感じですね。
Func<string, Func<string, Func<string, string>>> encloseC = open => close => str => open + str + close;
encloseC は、【文字列を取って、『文字列を取って、「文字列を取って文字列を返す関数」を返す関数』を返す関数】になっています。
呼び出しはこんな感じで。
writeLine(encloseC("{")("}")("enclosed!")); // => {enclosed!}
ところで、このカリー化形式の encloseC、引数を部分的に渡しておくことが出来ます。
var encloseCurly = encloseC("{")("}"); writeLine(encloseCurly("囲まれた!"));
こうやって部分適用することで、新しい関数が作れるわけです。 ちなみに Curly は、波カッコ=カーリーブラケット「{~}」のことで、カリー化とは関係ないのであしからず。
まとめ
Action 以降のソース、こんな感じです。
Action<string> writeLine = System.Console.WriteLine; Func<string, string> enclose = s => "[" + s + "]"; writeLine(enclose("foo")); Func<string, string> capitalize = s => s.Substring(0, 1).ToUpper() + s.Substring(1).ToLower(); writeLine(capitalize("foo")); // 関数合成 writeLine(capitalize.AndThen(enclose)("foo")); Func<string, string> middle = (string s) => s.Substring(s.Length / 3, s.Length - s.Length * 2 / 3); writeLine(middle.AndThen(capitalize).AndThen(enclose)("yoofoobar")); writeLine(enclose(capitalize(middle("foobaryah")))); // カリー化形式 Func<string, Func<string, string>> sandwich = tag => str => tag + str + tag; writeLine(sandwich("***")("sanded!")); Func<string, Func<string, Func<string, string>>> encloseC = open => close => str => open + str + close; writeLine(encloseC("{")("}")("enclosed!")); // 部分適用 var encloseCurly = encloseC("{")("}"); writeLine(encloseCurly("囲まれた!"));
おまけ:限界について
ラムダ式を変数で受けるスタイルは、書き方の問題を除けば、このくらいであれば問題なく使えます。 ですが、メソッドで実現できることをすべて実現できるわけではありません。 最も大きいのが、ジェネリックな関数を定義できない、というものです。 例えば、こんな簡単なメソッドを考えてみましょう。
T id<T>(T x) { return x; }
引数をそのまま返すだけの関数ですが、ジェネリックな定義になっており、string だろうが int だろうが使えます。
string s = id("hoge"); int i = id(42);
これを、ラムダ式を変数で受け取るスタイルで書こうとしても出来ません。
Func<T, T> id = x => x; // Tって何
無理やり回避するなら、クラスを作る必要があります。
public static class Id<T> { public static readonly Func<T, T> Apply = x => x; }
var str = Id<string>.Apply("hoge"); var i = Id<int>.Apply(42);
・・・面倒ですね。 そこで F# ですよ!