Kotlinでカリー化と関数部分適用
Kotlin Advent Calendarの5日目。まだ空きだらけなので2回目やるよ。今日は以前少し書いた内容をもっと見て行こうと思う。
カリー化
Kotlinにおける関数オブジェクトをカリー化する。あたかも最初からカリー化されてるように見せるためにこんなコードを書く。
fun <A, B, Result> Function2<A, B, Result>.invoke(a: A) = { (b: B) -> this(a, b) }
2引数関数に1引数のinvokeを拡張関数として定義。これがスコープにあるとき次のコードが期待通りに動く。
val f = minus(10) println(f(7)) // => 3 println(minus(5)(3)) // => 2
いい感じ。
関数の部分適用
上記のカリー化で、関数の部分適用はちょっとだけできる。1つ目の引数を確定させて2つ目の引数はまた後で。でもその逆、1つ目の引数は未定で2つ目の引数を確定させたい場合がある。この要望に対応したい。そこで次のコード。
enum class PlaceHolder { _ } fun <A, B, Result> Function2<A, B, Result>.invoke(_: PlaceHolder, b: B) = { (a: A) -> this(a, b) }
未定にしたい引数のところにプレースホルダとしてPlaceHolder._
を使う。これを別名インポートして_
って名前にしておけば次のコードが書ける。
val decrement = minus(_, 1) println(decrement(5)) // => 4 println(minus(_, 3)(8)) // => 5
3引数の関数に挑戦
この方法を使ってFuction22までのコードをひたすら書くだけ!とりあえず3引数に対応しよう。
fun <A, B, C, Result> Function3<A, B, C, Result>.invoke(a: A) = { (b: B, c: C) -> this(a, b, c) }
3引数関数が引数1つだけに適用されると2引数関数を返す。で、さっきのコードが連鎖してその2引数関数も1引数関数として使える。
println(minus(5)(3, 1)) // => 1
うん。
println(minus(5)(3)(1)) // => 1
あれ!?これはコンパイルが通らない!
println(minus(5)(3))
こうしてみてもダメ。No value passed for parameter p2
というエラーメッセージが。返される2引数関数で、1引数の拡張関数invoke
が使えないっぽい。。
仕方ない。こうしてみる。
fun <A, B, C, Result> Function3<A, B, C, Result>.invoke(a: A) = { (b: B) -> { (c: C) -> this(a, b, c) } }
これならprintln((minus(5)(3)(1)))
が上手く行くけど、当然println(minus(5)(3, 1))
はダメ。しかも、この形だと「2つ目や3つ目の引数で部分適用」はできない。たぶんコンパイラさん由来なので改善できる問題かも。
funKTionale
ちなみにこのようにKotlinで関数型プログラミングするためのライブラリがある。funKTionaleという名前でGithubに上がってるよ。
このライブラリは本エントリのようなアプローチではなく、プログラマが明示的にカリー化する方法を採っている。例えば関数f
をカリー化するにはf.curried()
と記述する必要がある。
部分適用するにはpartially
系の関数を使うんだけど1つ目の引数のみ適用の場合はpartially1
を、2つ目の引数のみ適用の場合はpartially2
という感じになる。
他にも関数合成やOption
型などがあったりする。