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の厳格な区別によってプログラマに「値の不在」の可能性の有無を意識させることができます。そういう意味で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を適用するときに自然な記法を用いることができます。