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

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

KotlinのNullable型をモナドっぽくしてみた

昨日、@seri_kさん主催の第五.五回 #渋谷javaに参加しました。そこで「Kotlin Nullable型をモナドっぽくしてみた」というタイトルで発表させていただきました。その内容をもう少し詳しく解説したいと思います。なおタイトルに含まれている「モナド」という言葉は本エントリには登場しません。

背景

Nullable型とは

KotlinのNull安全(Null-Safety)の仕組みとして、変数は非Null型(NotNull)Null許容型(Nullable)の2つに別れます。こんな具合にNotNullにはnullを代入できませんが、Nullableには代入できます。

val a: Int  = null // コンパイルエラー
val b: Int? = null // OK
val c: Int  = b    // コンパイルエラー

Nullable変数はnullの可能性があるので、メソッド呼び出しに特別な記法を用います。もしレシーバとなる変数がnullの場合はメソッド呼び出しによりNullPointerExceptionは起こらずnullが返されるだけです。

val a: Int? = 5
a?.plus(3) // => 8

val b: Int? = null
b?.plus(3) // => null

Nullable型の利点

このようなNotNullとNullableの厳格な区別によってプログラマに「値の不在」の可能性の有無を意識させることができます。そういう意味でJavaOptionalに非常に近いです。しかしKotlinの場合、言語レベルでサポートしているのでNullPointerExceptionはほぼ起こらなくなります。

Nullable型の欠点

上記の例のようにNullable型はNotNull型と見なせません。例えば次のようなシグネチャの便利な関数あるとします。

fun toInt(str: String): Int?

NotNullのStringを引数として取り、NullableのIntを返す関数です。NullableのStringをこの関数に直接適用することはできません。なぜなら引数としてNotNullを期待しているからです。

Kotlinでは、nullでないことを確認した直後のブロックでは変数がNotNullとして見なされます。そのことを利用してNullableのStringを、toIntに適用できます。

val str? = "123"
if(str != null) toInt(str) else null // => 123

なんとか、NotNullを取る関数にNullableを適用することができました。が、これは不便です。分岐があり、書きにくく読みにくいコードになってしまいます。

目的

「NotNullを取る関数」にNullableを適用する簡単な方法を得ることが目的です。

参考

ScalaOption, JavaOptionalが持つmapflatMapメソッドに似たものを目指します。mapは「通常の値を受け取って通常の値を返す関数」にOptionalを適用できるメソッドです。flatMapは「通常の値を受け取ってOptionalを返す関数」にOptionalを適用できます。

手法

次のような関数を導入します。

fun <T, R> T.bind(f: ((T) -> R)?): R? = f?.invoke(this)

任意の型Tに対する拡張関数です。「Tを取って任意の型Rを返す関数」を引数に取ります。そして、戻り値の型はR?です。関数の本体では、引数に取った関数にレシーバ自身を適用しています。簡単な使用例を示します。

8 bind { it + 7 }             // => 15
"abc" bind { "[" + it + "]" } // => [abc]
"null" bind { "hoge" }        // => hoge
null?.bind { "hoge" }         // => null

検証

目的の関数bindを得たので、これが実際に「NotNullを取る関数」にNullableを適用できるかどうかを検証します。

NotNull -> NotNull の関数に適用 (mapに相当)

fun square(i: Int): Int = i * i
val a: Int? = 5
val b: Int? = null

変数absquareに適用してみます。bindを使わない方法はこうです。

if(a != null) square(a) else null // => 25
if(b != null) square(b) else null // => nul

そしてbindを使った方法です。

a?.bind(::square) // => 25
b?.bind(::square) // => null

NotNullを取るsquareにInt?を渡して期待する結果を得ることができました!しかもif-elseが消えました。

NotNull -> Nullable の関数に適用 (flatMapに相当)

fun toInt(str: String): Int? = try { str.toInt() } catch(e: Exception) { null }
fun square(i: Int): Int = i * i
val a: String? = "5"
val b: String? = "a"
val c: String? = null

変数abctoIntに適用し、その結果をsquareに適用してみます。まずはbindを使わない方法から。

if(a != null) {
    val aInt = toInt(a)
    if(aInt != null) square(aInt) else null
} else {
    null
}

あちゃー、if-elseが入れ子になっちゃいました。次にbindを使った方法です。

a?.bind(::toInt)?.bind(::square) // => 25
b?.bind(::toInt)?.bind(::square) // => null
c?.bind(::toInt)?.bind(::square) // => null

cnullなので最初のbindでコケます。b"a"なので最初のbindtoIntへ渡ります。しかしその結果がnullなので次のbindでコケて最終的にnullとなります。

結果

検証により導入したbind関数がmapflatMapっぽくなり、Nullable型の取り扱いが簡単になったことがわかりました。

おまけ: Nullableな関数をbindに食わせる

bindの定義をもう一度見てください。

fun <T, R> T.bind(f: ((T) -> R)?): R? = f?.invoke(this)

引数fはNullableです。本体部がf?.invoke(this)となっており、fnullの場合はinvokeを呼び出さずにnullを返します。つまりbindの引数にnullを渡せます。

"hoge".bind(null) // => null

これの何が嬉しいのでしょうか。例えば次のような関数があったとします。

fun minus(a: Int, b: Int): Int = a - b

NotNullのIntを2つ取り、引き算の結果を返すだけの関数です。この関数にNullableを渡したいときにもbindが役立ちます。

まずminusをカリー化します。カリー化とは「複数の引数を取る関数」を「『1つの引数を取る関数』を返す1つの引数を取る関数.......」に変換することです。minusをカリー化するとこうなります。

val minus = { (a: Int) -> { (b: Int) -> a - b } }

カリー化されたminusは「aを取って『bを取ってIntを返す関数』を返す関数」になりました。こんな感じで使えます。

minus(6)(2)         // => 4

val foo = minus(10) // => {(b: Int) -> 10 - b}
foo(3)              // => 7

準備が整いました。NotNullを取るminusbindを使ってNullableな値を食わせてみましょう。

val x: Int? = 7
val y: Int? = 3
val z: Int? = null
  
// x - y
y?.bind(x?.bind(minus)) // => 4
  
// z - y
y?.bind(z?.bind(minus)) // => null

z - y の例を見てみます。znullなのでz?.bind(minus)の結果はnullとなりy?.bind(null)の形になります。繰り返しになりますが、bindnullを取ることもできるので型の不整合は起こりません。こうして最終的な結果がnullになります。

おまけのおまけ

x - y はつまり、カリー化してないminusを使うとminus(x, y)と表現できますがy?.bind(x?.bind(minus))と記述することになりxyの順番がひっくり返ってぱっと見でわかりにくいです。そこで次のような関数を追加します。

fun <T, R> Function1<T, R>.apply(nullable: T?): R? = nullable?.bind(this)

このapply関数を用いればy?.bind(x?.bind(minus))を次のように記述できます。

minus.apply(x)?.apply(y) // => 4

すごい!関数minusから初めてxyと順番にapplyへ渡しています。かなり読みやすくなりました。

まとめ

bind関数を導入することでNullable型が使いやすくなりました。つまり、NotNullを取る関数にNullabelを適用するときに自然な記法を用いることができます。