1. 概要

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

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

2. Mavenのセットアップ

この機能はKotlin1.3で導入されたため、このバージョン以降を使用する必要があります。 このチュートリアルでは、利用可能な最新バージョン –1.3.10を使用します。

設定の詳細については、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ではないことを知ることができます。 つまり、println命令が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. 戻り値に基づく保証の作成

ここでは、ターゲット関数が戻った場合にターゲット条件が満たされることを指定します。これはモチベーションセクションで使用しました。

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 コントラクトでは、の右側にある true false 、およびnullのみが許可されていることに注意してください。 は意味します。

そして、 示すかかりますブール値引数として、有効なKotlin式のサブセットのみが受け入れられます。 ヌル -チェック( == null = null)、インスタンスチェック( !は )、論理演算子( && || )。

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

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

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

callbacksInPlace コントラクトは、次の保証を表します。

  • 所有者関数が終了した後、callableは呼び出されません
  • また、契約なしで別の関数に渡されることはありません

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

inline fun <R> 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 <R> myRun(block: () -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

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

ここでは、InvocationKind.EXACTLY_ONCEを使用しました。 その他のオプションは、AT_LEAST_ONCE、AT_MOST_ONCE、およびUNKNOWNです。

5. 契約の制限

Kotlinの契約は有望に見えますが、現在の構文は現在不安定であり、将来完全に変更される可能性があります。

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

  • コントラクトは、ボディを持つトップレベルの関数にのみ適用できます。 フィールドやクラス関数では使用できません。
  • コントラクト呼び出しは、関数本体の最初のステートメントである必要があります。
  • コンパイラは無条件にコントラクトを信頼します。 これは、プログラマーが正しく健全な契約書を作成する責任があることを意味します。 将来のバージョンで検証が実装される可能性があります。

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

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 )は、通常の互換性をすべて保証します。

そのため、今でもコントラクトを使用する価値があることをお勧めします – DSLが変更された場合でも、コントラクト宣言を変更するのはそれほど難しくありません。

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