PHONE APPLI Engineer blog

エンジニアブログ

Kotlin 紹介 - Nothing 型の使い道

弊社プロダクト「PHONE APPLI PEOPLE (旧:連絡とれるくん)」では、サーバサイドに Kotlin を採用しています。
今回は、初見だとたぶん使い道が分かり辛い Nothing 型について紹介します。

Nothing 型とは

Kotlin language specification
言語仕様によると、ユーザが定義した型も含む、すべての型のサブタイプ (i.e. ボトム型) です。
すべての型のスーパータイプである Any のちょうど反対にある型です。
「すべての型のサブタイプ」という訳の分からないものなので、インスタンスが存在しません。 (コード上で Nothingインスタンスを作ることはできないです)

言語仕様だけだとよく分からない型ですが、使い道はちゃんと存在します。

Nothing 型の使い道

  • 関数/ラムダ式の戻り値の型に指定
  • 型引数に指定
  • nullable Nothing

自身の経験上、使い道としてはこれらがあると認識してます。
それぞれの詳細について記述していきます。

戻り値が Nothing の関数は必ず例外を投げる

戻り値の型を Nothing にすると、必ず例外を投げる関数になります。
(Nothing 型のインスタンスは存在しない → 戻り値を return できない → 例外を投げるしかない、という理屈らしい・・・)
別の言い方をすると throw 例外() を関数化できるということです。

例えば「TODO として実装を後回しにしたい」場合に、標準ライブラリに TODO 関数が用意されており、戻り値の型が Nothing です。

fun doSomething(): SomethingResult {
    TODO("後回し")
}

見た目が分かり易い + 戻り値が無くてもコンパイルを通すことができるので、便利な関数だと思います。

戻り値が Nothing のラムダ式は例外を投げる or 呼び出し元から return

戻り値の型が Nothingラムダ式が例外を投げるのは関数と同じですが、ラムダ式を引数とする関数が inline fun だった場合、 return による関数の呼び出し元からの脱出も可能です。

inline fun + Nothing ラムダ式のサンプルコードです。

// レシーバから null を取り除く関数
// null の場合は handleNull が実行される
inline fun <T> T?.rejectNull(handleNull: () -> Nothing): T =
    this ?: handleNull()

// 使用例
fun doSomething() {
    // null かもしれない結果を返す関数
    val result = getNullableResult().rejectNull {
        println("結果が null なので doSomething から脱出")
        return
        // return の代わりに throw でも可
    }

    // non-null な result に対して処理
}

型引数を省略できない場合に Nothing を指定できる

emptyList を変数に置く場合、型引数は何を使うのがベストでしょうか?
どんな型を型引数にしても問題は無いのですが、空の読み取り専用リスト (= 値が追加されることはない) であるため、 Nothing がふさわしいと思います。

あまり無いコードだとは思いますが、 List<Nothing> 変数を空でないリストとあわせて使う場合も、型推論がうまく動作してくれます。

val empty = emptyList<Nothing>()

listOf(
    listOf(1),
    empty
).forEach {
    println(it) // it は List<Int> に型推論される
}

Nothing がすべての型のサブタイプであるため、型推論候補に他のタイプがあればそちらが優先される、のような感じだと推測してます。

また、emptyList() を変数に置かない場合でも、書き方によっては型推論してくれない場合があります。

// Kotlin 1.4.31 で確認
listOf(
    listOf("a") to true,
    // emptyList に型引数を指定しないとコンパイルエラー
    emptyList() to false
).forEach { (list, flag) ->
    println("list: $list | flag: $flag")
}

型引数に String を指定するのもアリですが、代わりに Nothing でもコンパイル可能です。
特に、型引数が入れ子になって長くなる場合 (例: Map<String, ByteArray>) に Nothing と短く書けるのは便利です。

Nothing? 型は null しか入らない入れ物

nullable な Nothing 型は null しか入れることができません。
null しか入らないものの使い道はあまり無いのですが、 1 つだけ紹介できます。

ライブラリ・フレームワーク都合で、意味の無い引数を用意しなければならない場合があります。
その場合、 meaningless: Nothing? = null のような引数がちょうどいい感じだと思います。

  • デフォルト値があるため、呼び出し側で引数を書かなくていい
  • 間違って引数を書いても null 以外入れることができない
    → 指定する必要の無いものだと気付くことができる

実例として mybatis があります。

interface SomethingMapper {
    // mybatis mapper 関数の特性として引数が「参照型 1 つ」の場合、
    // SQL 内で使えるバインド変数 #{xxx} はそのプロパティになる
    // ↓
    // この関数に meaningless 引数が無いと #{ids[0]} のようなバインド変数が使えない
    //
    // ※ 引数を増やす以外に、 @Param("ids") を引数に付ける方法もある
    @Select("SELECT * FROM something WHERE id IN (#{ids[0]}, #{ids[1]})")
    fun findSomethingByIds(
        ids: List<String>,
        meaningless: Nothing? = null
    ): List<Something>
}

終わりに

Kotlin の Nothing 型の使い道について、自身の経験上から紹介してみました。

私自身、ボトム型の概念を初めて知ったのが Kotlin だったため、最初のうちは何のためにあるのかさっぱりでした。
ただ、その言語に触り続けているといつの間にか有用性に気付けたりします。



書いた人: プロダクトデベロップメント部 藤本泰輔