Java 8を関数型っぽく使うためのおまじないをScalaで(ry
はじめに
言うまでもなく下記の記事のパクリです。Java と C# と F# があるのに Scala が無いなんて、と謎の使命感に駆られた結果がこれだよ!
- Java 8を関数型っぽく使うためのおまじない(きしだのはてな)
- Java 8を関数型っぽく使うためのおまじないをC#でやってみた(ぐるぐる〜)
- Java 8を関数型っぽく使うためのおまじないをF#でやってみた(ぐるぐる〜)
まずは、素晴らしい記事を公開して頂いたご両名に感謝を。というか、こういう記事をさらさらと書き下せる方は本当にすごいと思いました。ぼくにはとてもできない。
Function1 型
Java 8 ではFunction
型が追加されましたが、Scala はオブジェクト指向と関数型のハイブリッドなので、最初からFunction1
という型があります。パッケージ名や型引数まで含めると scala.Function1[-T1, +R] です。T1
を引数に取ってR
を返す関数、と読みます。
こんな感じで使います。
// Function<String, String> enclose = s -> "[" + s + "]"; val enclose: Function1[String, String] = s => "[" + s + "]"
String
を引数にとってString
を返すenclose
という関数を定義しました。
また、Scala には型推論があるので次のようにFunction1
型の宣言を省略できます。
val enclose = (s: String) => "[" + s + "]"
この関数に引数を与えて呼び出すと [foo]
と表示されます。
println( enclose("foo") )
もうひとつ関数を定義してみましょう。最初の文字を大文字にする関数です。ちなみに、Scala は JVM 言語なので Java の機能をそのまま使えます。
// Function<String, String> capitalize = s -> // s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase(); val capitalize = (s: String) => s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase()
同様に引数を与えて呼び出すとFoo
と表示されます。
println( capitalize("foo") )
この二つを順に呼び出す、つまりcapitalize
してenclose
すると[Foo]
と表示されます。
// System.out.println( enclose.apply(capitalize.apply("foo")) ); println( enclose(capitalize("foo")) )
こういう場合、Java と同様にandThen
メソッドを使うと二つの関数を連結できます。Scala は中置記法が使えるので、可読性を下げるカッコを減らすことができます。
// System.out.println( capitalize.andThen(enclose).apply("foo") ); println( (capitalize andThen enclose)("foo") )
これを関数合成といいます。合成した関数に新しい名前を与えて使うこともできます。
val capEnc = capitalize andThen enclose println( capEnc("foo") )
Scala で関数型っぽいことができることがわかりました。やりましたね!
関数の定義
ところで、ここまで関数をFunction1
型のインスタンスとして扱ってきましたが、def
を使って明示的に関数として定義することもできます。この場合、引数は Java と同様にfunc(a: A)
の形式で書きますが、書き方はval
で定義する場合とほとんど同じです。
// enclose: String => String def enclose(s: String) = "[" + s + "]" // capitalize: String => String def capitalize (s: String) = s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase()
同様に関数合成もできます。やはり、Function1
型の時と同じ記法が使えることが分かります。
// capEnc: String => String def capEnc = (capitalize _) andThen enclose println( capEnc("foo") )
別名を使う
Scala では、Function1[-T1,+R]
の別名としてT1 => R
が使えるのでおまじないは要りません。
val enclose: String => String = s => "[" + s + "]"
ところで、1 引数のFunction1[-T1,+R]
があるってことは、2 引数のFunction2[-T1,-T2,+R]
や 3 引数のFunction3[-T1,-T2,-T3,+R]
も同様にあるんじゃないかと思うかもしれませんが、2 引数以上の関数は甘えなので、1 引数の関数さえ知っていれば問題ありません。
関数合成
先ほども述べたように、関数合成は以下のように書けます。
println( (capitalize andThen enclose)("foo") )
さらにもう一つ関数を定義して次のように書いてみます。まんなかあたりを取り出す関数middle
です。
val middle = (s: String) => s.substring(s.length() / 3, s.length() * 2 / 3) // println._( middle.x(capitalize).x(enclose)._("yoofoobar") ); println( (middle andThen capitalize andThen enclose)("yoofoobar") )
関数合成を使わないと、次のようになりますね。
// println._( enclose._(capitalize._(middle._("foobaryah")))); println( enclose(capitalize(middle("foobaryah"))) )
このように、実際に呼び出す順と記述が逆になります。最初にmiddle
して、次にcapitalize
して、最後にenclose
したいのに、まずenclose
から書く必要があります。
また、カッコがたまっていくので、最後にまとめてカッコを閉じる必要があります。ちょっと関数呼び出しが増えると、閉じカッコの数がよくわからなくなってコンパイルエラーがなくなるところまで「)」を増やしたり減らしたりなんてこと、ありますよね。
Function1
に次のようなメソッドを追加しておくとさらにいいです。
implicit class Function1Ops[-T1,+R](val f: T1 => R) extends AnyVal { def して[R2](g: R => R2): T1 => R2 = f andThen g def するのを(a: T1) = f(a) }
こうなって読みやすいですね。
// println._( middle.して(capitalize).して(enclose).するのを("yoofoobar") ); println( capitalize して enclose して middle するのを "yoofoobar" )
…読みやすいですね?
カリー化
さて、2 引数以上の関数は甘えと書きましたが、実際 2 つ以上のパラメータを渡したいときはどうすればいいんでしょう?
こういうときに使うのがカリー化です。カリー化は、ひとつの引数をとって関数を返すことで、複数のパラメータに対応します。
例えば、挟む文字と挟まれる文字を指定すると文字を文字で挟む関数sandwich
を、通常の 2 引数関数で表すとこうなります。
// String sandwich(String enc, String str){ // return enc + str + enc; // } def sandwich(enc: String, str: String) = enc + str + enc
これを 1 引数関数でカリー化して書くと次のようになります。
/// F<String, F<String, String>> sandwich = enc -> str -> enc + str + enc; val sandwich: String => String => String = enc => str => enc + str + enc;
sandwich
自体は、文字列を引数に取って、”文字列を引数に取って文字列を返す関数” を返す関数になっています。
この関数に 2 引数を与えて呼び出すと***sanded!***
と表示されます。
// println._( sandwich._("***")._("sanded!") ); println( sandwich("***")("sanded!") )
3 引数だとこんな感じですね。
// F<String, F<String, F<String, String>>> encloseC = pre -> post -> s -> pre + s + post; val encloseC: String => String => String => String = pre => post => s => pre + s + post
encloseC
は、文字列を引数に取って、”文字列を引数に取って、”文字列を引数に取って文字列を返す関数” を返す関数” を返す関数になっています。
この関数に 3 引数を与えて呼び出すと{enclosed!}
と表示されます。
// println._( encloseC._("{")._("}")._("enclosed!") ); println( encloseC("{")("}")("enclosed!") )
ところで、このカリー化されたencloseC
、引数を部分的に渡しておくことができます。
// F<String, String> encloseCurly = encloseC._("{")._("}"); val encloseCurly = encloseC("{")("}") println( encloseCurly("囲まれた!") )
こうやって部分適用することで、新しい関数が作れるわけです。ちなみに Curly は波カッコ=カーリーブラケット{}
のことで、カリー化とは関係ないのであしからず。
さて、同じように 4 引数だと…え、もう=>
は書きたくない?
大丈夫、Scala はそんなあなたのためにcurried
メソッドを用意しています。これを使えば、2 引数以上の関数をカリー化された関数へと簡単に変換できるのです。curried
は 2 引数以上のあらゆる関数で使えるので安心!
// fourArgFunc: String => String => String => String => String val fourArgFunc = ((a: String, b: String, c: String, d: String) => a + b + c + d).curried println( fourArgFunc("a")("b")("c")("d") )
2 引数以上の関数は甘え? 何のことですか?
まとめ
以上をまとめて書くと、こんな感じです?
ちなみに、はてなの markdown 記法は Scala に対応してるっぽくて素晴らしい。
val enclose = (s: String) => "[" + s + "]" println( enclose("foo") ) val capitalize = (s: String) => s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase() println( capitalize("foo") ) //関数合成 println( (capitalize andThen enclose)("foo") ) val middle = (s: String) => s.substring(s.length() / 3, s.length() * 2 / 3) println( (middle andThen capitalize andThen enclose)("yoofoobar") ); println( enclose(capitalize(middle("foobaryah")))) println( middle して capitalize して enclose するのを "yoofoobar" ) //カリー化 val sandwich: String => String => String = enc => str => enc + str + enc; println( sandwich("***")("sanded!") ) val encloseC: String => String => String => String = pre => post => s => pre + s + post println( encloseC("{")("}")("enclosed!") ) val encloseCurly = encloseC("{")("}") println( encloseCurly("囲まれた!") ) val fourArgFunc = ((a: String, b: String, c: String, d: String) => a + b + c + d).curried println( fourArgFunc("a")("b")("c")("d") )
おまけ
関数型プログラミングの技法を関数型言語で書いた方が自然に書けるのは当たり前です。どこかで読んだ話によれば、Java 8 のラムダ式は関数型プログラミングのサポートが第一目的ではないとのことで、ならば、できないこともそれなりにあって当然です。
とはいえ、ラムダ式の導入によって Java プログラマにとっても関数型プログラミングの考え方や方法論がグッと身近になるのも事実。
というわけで、この機会に少し関数型プログラミングの考え方を勉強してみるのはいかがでしょうか? たとえ実際の仕事に使う機会がなくとも、あなたのプログラミングの芸風を広げるのにきっと役立つはずです。
おまけ2
関数型プログラミングへの導入に役立ちそうなポインタをいくつか。
まず、IIJ の山本和彦さんが Java プログラマ向けの関数型プログラミングの紹介記事 ”他の言語を学んで自由になろう” を公開されています。
また、手前味噌ですが Java プログラマ向けの Scala 紹介スライドを公開しています。このスライドでは関数型プログラミングについての話題はほとんど省いていますが、もし Scala という言語自体にご興味があればどうぞ。