LoginSignup
2
1

More than 3 years have passed since last update.

Kotlin 1.4-M1がリリースされたのでContractsの動向をチェックしてみた

Posted at

はじめに

先日、Kotlin 1.4のプレビューバージョンであるKotlin 1.4-M1のリリースがありました。Kotlin 1.4-M1では型推論、Contracts、コンパイラーなどの機能改善がありましたが、その中でもContractsのアップデート情報を紹介したいと思います。

Kotlin Contractsとは

Kotlin 1.3からプログラムの正確性の向上を支援するためにExperimentalな機能としてContractsが導入されました。コンパイラに対して契約を宣言することで快適なコーディングの支援に繋がります。標準ライブラリでもいくつか利用されていて、例えばスコープ関数などでContractsが利用されています。

Kotlin 1.4-M1のContractsの動向

Kotlin 1.4-M1では、Contractsの機能改善といくつかの標準ライブラリにConstractsが利用されていますので、それぞれ紹介したいと思います。まず初めに具体化型パラメータへの参照を許可されました。対象のIssueはこちらです。

@OptIn(ExperimentalContracts::class)
inline fun <reified T> assertIsInstance(value: Any?) {
    contract {
        returns() implies (value is T)
    }

    assertTrue(value is T)
}

上記の関数ではインスタンス型検証を行なっています。T型パラメータが具象化されているため、関数内でその型を確認することができ、Contractsを利用できるようになりました。上記の関数は今後、kotlin.testライブラリに追加されるようです。

次にfinalのメンバー関数にConstractsが利用可能となりました。対象のIssueはこちらです。また対象のPRを見てみると、下記のようにfinalのメンバー関数でContractsが利用できることがわかります。今まではトップレベル関数でしかContractsを利用できなかったことを考えると、大きなサポートと言えます。

contractCallSites.1.4.kt
open class Class {
    fun member(x: Boolean) {
        contract { returns() implies (x) }
    }

    inline fun inlineMember(x: Boolean) {
        contract { returns() implies (x) }
    }

    abstract fun abstractMember(x: Boolean) {
        <!CONTRACT_NOT_ALLOWED!>contract<!> { returns() implies (x) }
    }

    open fun openMemeber(x: Boolean) {
        <!CONTRACT_NOT_ALLOWED!>contract<!> { returns() implies (x) }
    }

    suspend fun suspendMember(x: Boolean) {
        contract { returns() implies (x) }
    }
}

次にmeasureTimeMillis()、measureNanoTime()の標準ライブラリにもConstractsが利用されていますので、それぞれ内容を確認してみましょう。対象のIssueはこちらです。

Timing.kt
public inline fun measureTimeMillis(block: () -> Unit): Long {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE) // Kotlin 1.4でContractsが利用された
    }
    val start = System.currentTimeMillis() |
    block()
    return System.currentTimeMillis() - start |
}

public inline fun measureNanoTime(block: () -> Unit): Long {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE) // Kotlin 1.4でContractsが利用された
    }
    val start = System.nanoTime()
    block()
    return System.nanoTime() - start
}

measureTimeMillis()、measureNanoTime()は特定のコードブロックの実行時間をベンチマークするための関数です。Contractsが利用されていることがわかります。次のmeasureTimeMillis()を呼び出すコードではコンパイルが通るようになります。

val x: Int
measureTimeMillis {
    x = 1 // コンパイルOK(Kotlin 1.4)
}

println(x) // コンパイルOK(Kotlin 1.4)

ですが、Contractsが利用されていないKotlin 1.3で実行してみるとコンパイルエラーになります。

val x: Int
measureTimeMillis {
    x = 1 // コンパイルNG(Kotlin 1.3)
}

println(x) // コンパイルN(Kotlin 1.3)

なぜダメなのでしょうか。その理由ですが、measureTimeMillis()のラムダ内では、再代入できない変数xが再割り当ての可能性があるため、初期化が禁止されているためです。後続のprintln(x)の部分では、変数xが初期化されていないと認識されます。

measureTimeMillis()内のcontractのラムダ内を見てみると、callsInPlace(block, InvocationKind.EXACTLY_ONCE)が定義されています。これは変数blockが一回だけ実行されることを定義しています。つまり、先ほどの変数xがmeasureTimeMillis()のラムダ内で一回しか実行されないため、変数xに1が初期化されて、後続処理である println(x)が1を出力する結果となります。

このようにしてmeasureTimeMillis()、measureNanoTime()にContractsが利用されました。では最後にuse()の標準ライブラリにもContractsが利用されているので見てみましょう。use()とはリソースの使用後にuse()を呼び出すことで、リソースの解放を行われてリソースの解放漏れを防ぐことができます。次のコードがuse()の実装内容で、対象のIssueはこちらです。

Closeable.kt
public inline fun <T : Closeable?, R> T.use(block: (T) -> R): R { 
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE) // Kotlin 1.4でContractsが利用された
    }   
    var exception: Throwable? = null
    try {
        return block(this)
    } catch (e: Throwable) {
        exception = e 
        throw e
    } finally {
        when {
            apiVersionIsAtLeast(1, 1, 0) -> this.closeFinally(exception)
            this == null -> {}
            exception == null -> close()
            else ->
                try {
                    close()
                } catch (closeException: Throwable) {
                    // cause.addSuppressed(closeException) // ignored here
                }   
        }   
    }   
}

以下のuse()の呼び出しコードでは、再代入不可の変数xをuse()のラムダ内で初期化しています。今まではuse()のラムダ内で変数xの初期化ができなかったのですが、contract内でcallsInPlace(block, InvocationKind.EXACTLY_ONCE)を定義しているため、ラムダ内の処理が一回だけと定義しています。その結果、変数xの初期化が正常に行われて、println(x)の結果として1が出力されます。

val x: Int
"".byteInputStream().use {
   x = 1 // コンパイルOK(Kotlin 1.4)
}

println(x) // コンパイルOK(Kotlin 1.4)

Kotlin 1.3ではuse()にContractsが利用されていないため、以下のようにコンパイルエラーになります。

val x: Int
"".byteInputStream().use {
   x = 1 // コンパイルNG(Kotlin 1.3)
}

println(x) // コンパイルNG(Kotlin 1.3)

おわり

Kotlin 1.4 M1がリリースされましたので、Contractsのアップデート情報を紹介しました。ContractsはExperimentalな機能ですが、Kotlin 1.4のリリースがとても待ち遠しいですね。引き続き動向を追っていこうと思います。

参考

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1