算譜王におれはなる!!!!

偏りはあると思うけど情報技術全般についてマイペースに書くよ。

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型などがあったりする。