きしだのはてな このページをアンテナに追加 RSSフィード

2013-05-01(水) Java 8を関数型っぽく使うためのおまじない

[][]Java 8を関数型っぽく使うためのおまじない 06:37 Java 8を関数型っぽく使うためのおまじないを含むブックマーク

やあ、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._("囲まれた!") );

dankogaidankogai 2013/05/01 16:50 ***sanded!*** ってサンドペーパーかけたの?
Sandwitchというのは人の名前由来なので、日本語的省略すると残念なことになる。
Dan the Nitpicker

nowokaynowokay 2013/05/01 17:12 そういうツッコミをうけるための遊びでしたw