昨日、@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の厳格な区別によってプログラマに「値の不在」の可能性の有無を意識させることができます。そういう意味でJavaのOptionalに非常に近いです。しかし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を適用する簡単な方法を得ることが目的です。
参考
ScalaのOption, JavaのOptionalが持つmapやflatMapメソッドに似たものを目指します。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
変数a、bをsquareに適用してみます。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
変数a、b、cをtoIntに適用し、その結果を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
cはnullなので最初のbindでコケます。bは"a"なので最初のbindでtoIntへ渡ります。しかしその結果がnullなので次のbindでコケて最終的にnullとなります。
結果
検証により導入したbind関数がmapやflatMapっぽくなり、Nullable型の取り扱いが簡単になったことがわかりました。
おまけ: Nullableな関数をbindに食わせる
bindの定義をもう一度見てください。
fun <T, R> T.bind(f: ((T) -> R)?): R? = f?.invoke(this)
引数fはNullableです。本体部がf?.invoke(this)となっており、fがnullの場合は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を取るminusにbindを使って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 の例を見てみます。zはnullなのでz?.bind(minus)の結果はnullとなりy?.bind(null)の形になります。繰り返しになりますが、bindはnullを取ることもできるので型の不整合は起こりません。こうして最終的な結果がnullになります。
おまけのおまけ
x - y はつまり、カリー化してないminusを使うとminus(x, y)と表現できますがy?.bind(x?.bind(minus))と記述することになりxとyの順番がひっくり返ってぱっと見でわかりにくいです。そこで次のような関数を追加します。
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から初めてx、yと順番にapplyへ渡しています。かなり読みやすくなりました。
まとめ
bind関数を導入することでNullable型が使いやすくなりました。つまり、NotNullを取る関数にNullabelを適用するときに自然な記法を用いることができます。