やあ、3月に延期になったとはいえ、Java 8リリースが具体化してきましたね。
もうこれで、Lambdaがはずれるとかいうことはなさそうです。
ところで、Java 8で関数型っぽいことができるようになってうれしいのですが、ちょっと記述が冗長です。ということで、短く書けるおまじない考えてみました。
Function型
さて、まずはJava 8で標準で入ったFunction型をみてみましょう。パッケージ名まで含めるとjava.util.funciton.Functionです。
こんな感じで使います。
Function<String, String> enclose = s -> "[" + s + "]";
Genericsでの型指定の最初が引数、あとが戻り値の型です。ここではStringをとってStringを返す関数としてencloseを定義しています。
これを呼び出そうとすると、こんな感じになります。
System.out.println( enclose.apply("foo") );
こうすると、次のような表示になります。
[foo]
もうひとつ関数を定義してみましょう。最初の文字を大文字にする関数です。
Function<String, String> capitalize = s -> s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase();
2文字未満の文字列を与えると死にますが、コード例ってことで大目にみてください。
呼び出してみます。
System.out.println( capitalize.apply("foo") );
こんな感じの表示になります。
Foo
この2つを順に呼び出して、capitalizeしてencloseしようとすると、こんな感じになりますね。
System.out.println( enclose.apply(capitalize.apply("foo")) );
表示はこうなります。
[Foo]
こういう場合、andThenを使うと連結することができます。
System.out.println( capitalize.andThen(enclose).apply("foo") );
これは、関数合成として使うことができます。
Function<String, String> capEnc = capitalize.andThen(enclose);
System.out.println( capEnc.apply("foo"));
Java8で関数型っぽいことができることがわかりました。やりましたね!
別名を使う
さて、Functionって長いので、Fとか短く書きたいですね。
そんなときには、interfaceを定義してしまいます。
interface F<T, R> extends Function<T, R> {}
F<String, String> enclose = s -> "[" + s + "]"; F<String, String> capitalize = s -> s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase();
これで定義がすっきりしました。でも呼び出しにはあいかわらずapplyを書く必要があります。
それに、次の関数合成は怒られちゃいます。
F<String, String> capEnc = capitalize.andThen(enclose);
どうにかしないとね。
おまじない
別名だけではちょっと不便なので、applyやandThen相当のメソッドも短く定義しなおします。
interface F<T, R> extends Function<T,R>{ default R _(T t){ return apply(t); } default <V> F<T,V> x(F<? super R, ? extends V> after){ return (T t) -> after.apply(apply(t)); } } interface C<S> extends Consumer<S>{ default void _(S s){ accept(s); } }
こうすると、次のようになります。Consumerもついでに短くしたので、うっとおしいSystem.out.printlnにも別名がつけれます。
C<String> println = System.out::println; F<String, String> enclose = s -> "[" + s + "]"; println._( enclose._("foo") ); F<String, String> capitalize = s -> s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase(); println._( capitalize._("foo") );
ここで、1引数のFunctionだけ再定義したけど、2引数のBiFunctionとかのバリエーションも必要なんじゃないかと思うかもしれませんが、2引数以上の関数は甘えなので、1引数の関数が定義できれば問題ありません。
関数合成
これで、関数合成も次のように書けます。
println._( capitalize.x(enclose)._("foo") );
さらにもうひとつ関数を用意して次のように書いてみます。まんなかあたりを取り出す関数です。
F<String, String> middle = s -> s.substring(s.length() / 3, s.length() * 2 / 3); println._( middle.x(capitalize).x(enclose)._("yoofoobar") );
関数合成を使わないと、次のようになりますね。
println._( enclose._(capitalize._(middle._("foobaryah"))));
このように、実際に呼び出す順と記述が逆になります。middleしてcapitalizeしてencloseするのに、先にencloseから書く必要があります。
また、カッコがたまっていくので、最後にまとめてカッコを閉じる必要があります。ちょっと関数呼び出しが増えると、閉じカッコの数がよくわからなくなってコンパイルエラーがなくなるところまで「)」を増やしたり減らしたりなんてこと、ありますよね。
先ほどのFに次のようなメソッドを追加しておくとさらにいいです。
default R するのを(T t){ return _(t); } default <V> F<T,V> して(F<? super R, ? extends V> after){ return x(after); }
こうなって読みやすいですね。
println._( middle.して(capitalize).して(enclose).するのを("yoofoobar") );
カリー化
さて、2引数以上の関数は甘えと書きましたが、実際2つ以上のパラメータを渡したいときはどうすればいいんでしょう?
こういうときに使うのがカリー化です。カリー化は、ひとつの引数をとって関数を返すことで、複数のパラメータに対応します。
例えばはさむ文字とはさまれる文字を指定して、文字を文字ではさむ関数、通常の2引数関数であらわすなら次のようなsandwichがあるとします。
String sandwich(String enc, String str){
return enc + str + enc;
}
これを1引数関数でカリー化して書くと次のようになります。
F<String, F<String, String>> sandwich = enc -> str -> enc + str + enc;
sandwich自体は、文字列をとって、「文字列をとって文字列を返す関数」を返す関数になっています。
呼び出しは次のようになります。
println._( sandwich._("***")._("sanded!") );
表示は次のようになります。
***sanded!***
3引数だとこんな感じですね。
F<String, F<String, F<String, String>>> encloseC = pre -> post -> s -> pre + s + post;
encloseCは、文字列をとって、「文字列をとって、「文字列をとって文字列を返す関数」を返す関数」を返す関数になっています。
呼び出しはこんな感じで。
println._( encloseC._("{")._("}")._("enclosed!") );
表示はこうなります。
{enclosed!}
ところで、このカリー化されたencloseC、引数を部分的にわたしておくことができます。
F<String, String> encloseCurly = encloseC._("{")._("}"); println._( encloseCurly._("囲まれた!") );
こうやって部分適用することで、新しい関数が作れるわけです。
ちなみにCurlyは、波カッコ=カーリーブラケット「{〜}」のことでカリー化とは関係ないのであしからず。
まとめ
おまじない適用以降のソース、こんな感じです。ちょっとこれJavaだって言ってもわかってもらえなさそうですね。
はてな記法で一応 「>|java| 〜 ||<」って指定してるんですけど、ほんとにJava認識されるんだろうかw
C<String> println = System.out::println; F<String, String> enclose = s -> "[" + s + "]"; println._( enclose._("foo") ); F<String, String> capitalize = s -> s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase(); println._( capitalize._("foo") ); //関数合成 println._( capitalize.x(enclose)._("foo") ); F<String, String> middle = s -> s.substring(s.length() / 3, s.length() * 2 / 3); println._( middle.x(capitalize).x(enclose)._("yoofoobar") ); println._( enclose._(capitalize._(middle._("foobaryah")))); println._( middle.して(capitalize).して(enclose).するのを("yoofoobar") ); //カリー化 F<String, F<String, String>> sandwich = enc -> str -> enc + str + enc; println._( sandwich._("***")._("sanded!") ); F<String, F<String, F<String, String>>> encloseC = pre -> post -> s -> pre + s + post; println._( encloseC._("{")._("}")._("enclosed!") ); F<String, String> encloseCurly = encloseC._("{")._("}"); println._( encloseCurly._("囲まれた!") );