コトリンの契約

コトリン契約

1. 概要

このチュートリアルでは、Kotlin Contractsについて説明します。 それらの構文はまだ安定していませんが、バイナリ実装は安定しており、Kotlinstdlib itselfはすでにそれらを使用しています。

基本的に、Kotlinコントラクトは、関数の動作についてコンパイラーに通知する方法です。

2. Mavenセットアップ

この機能はKotlin 1.3で導入されたため、このバージョン以降を使用する必要があります。 このチュートリアルでは、we’ll use the latest version available –1.3.10。

設定の詳細については、introduction to Kotlinを参照してください。

3. 契約の動機

コンパイラは賢いのですが、必ずしも最良の結論に達するとは限りません。

以下の例を検討してください。

data class Request(val arg: String)

class Service {

    fun process(request: Request?) {
        validate(request)
        println(request.arg) // Doesn't compile because request might be null
    }
}

private fun validate(request: Request?) {
    if (request == null) {
        throw IllegalArgumentException("Undefined request")
    }
    if (request.arg.isBlank()) {
        throw IllegalArgumentException("No argument is provided")
    }
}

validateの呼び出しで例外がスローされない場合、プログラマーは誰でもこのコードを読んで、requestnullではないことを知ることができます。 In other words, it’s impossible for our println instruction to throw a NullPointerException.

残念ながら、コンパイラはそれを認識しておらず、request.argを参照することを許可していません。

ただし、関数が正常に返される場合、つまり例外をスローしない場合、指定された引数はnullではないことを定義するコントラクトによって、validateを拡張できます。

@ExperimentalContracts
class Service {

    fun process(request: Request?) {
        validate(request)
        println(request.arg) // Compiles fine now
    }
}

@ExperimentalContracts
private fun validate(request: Request?) {
    contract {
        returns() implies (request != null)
    }
    if (request == null) {
        throw IllegalArgumentException("Undefined request")
    }
    if (request.arg.isBlank()) {
        throw IllegalArgumentException("No argument is provided")
    }
}

次に、この機能について詳しく見ていきましょう。

4. コントラクトAPI

一般的な契約形態は次のとおりです。

function {
    contract {
        Effect
    }
}

これは、「関数を呼び出すと効果が生じる」と読むことができます。

次のセクションでは、言語が現在サポートしているエフェクトの種類を見てみましょう。

4.1. 戻り値に基づいた保証の作成

Here we specify that if the target function returns, the target condition is satisfied.これをMotivationセクションで使用しました。

returnsに値を指定することもできます。これは、ターゲット値が返された場合にのみ条件が満たされることをKotlinコンパイラに指示します。

data class MyEvent(val message: String)

@ExperimentalContracts
fun processEvent(event: Any?) {
    if (isInterested(event)) {
        println(event.message)
    }
}

@ExperimentalContracts
fun isInterested(event: Any?): Boolean {
    contract {
        returns(true) implies (event is MyEvent)
    }
    return event is MyEvent
}

これは、コンパイラがprocessEvent関数でスマートキャストを作成するのに役立ちます。

現在のところ、returnsコントラクトでは、 implies.の右側にあるtruefalse、およびnullのみが許可されていることに注意してください

また、impliesBoolean引数を取りますが、有効なKotlin式のサブセットのみが受け入れられます。つまり、null-checks(== null! = null )、インスタンスチェック(is!is)、論理演算子(&&||!)。

null以外の戻り値を対象とするバリエーションもあります。

contract {
    returnsNotNull() implies (event is MyEvent)
}

4.2. 関数の使用法について保証する

callsInPlace契約は、次の保証を表します。

  • 所有者関数が終了した後、callableは呼び出されません

  • また、契約なしで別の機能に渡されることはありません

これは、次のような状況で役立ちます。

inline fun  myRun(block: () -> R): R {
    return block()
}

fun callsInPlace() {
    val i: Int
    myRun {
        i = 1 // Is forbidden due to possible re-assignment
    }
    println(i) // Is forbidden because the variable might be uninitialized
}

指定されたブロックが1回だけ呼び出され、呼び出されることが保証されるようにコンパイラーを支援することで、エラーを修正できます。

@ExperimentalContracts
inline fun  myRun(block: () -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

標準のKotlinユーティリティ関数runwithapplyなどはすでにそのようなコントラクトを定義しています。

ここではInvocationKind.EXACTLY_ONCEを使用しました。 Other options are AT_LEAST_ONCEAT_MOST_ONCE, and UNKNOWN.

5. 契約の制限

Kotlinの契約は有望に見えますが、the current syntax is unstable at the moment, and it’s possible that it will be completely changed in the future.

また、いくつかの制限があります。

  • ボディを使用してトップレベルの関数にのみコントラクトを適用できます。 フィールドやクラス関数では使用できません。

  • コントラクトコールは、関数本体の最初のステートメントでなければなりません。

  • コンパイラは、無条件に契約を信頼します。これは、プログラマーが正しい契約書を作成する責任があることを意味します。 A future version may implement verification.

そして最後に、契約の説明ではパラメーターへの参照のみが許可されます。 たとえば、以下のコードはコンパイルされません。

data class Request(val arg: String?)

@ExperimentalContracts
private fun validate(request: Request?) {
    contract {
        // We can't reference request.arg here
        returns() implies (request != null && request.arg != null)
    }
    if (request == null) {
        throw IllegalArgumentException("Undefined request")
    }
    if (request.arg.isBlank()) {
        throw IllegalArgumentException("No argument is provided")
    }
}

6. 結論

この機能はかなり面白く見えます。構文はプロトタイプ段階ですが、バイナリ表現は十分に安定しており、すでにstdlibの一部です。 正常な移行サイクルがなければ変更されません。つまり、コントラクトのあるバイナリアーティファクトに依存できます(例: stdlib)は、通常の互換性をすべて保証します。

That’s why our recommendation is that it’s worth using contracts even now – DSLが変更された場合、契約宣言を変更するのはそれほど難しくありません。

いつものように、この記事で使用されているソースコードはover on GitHubで入手できます。