kotlin.contractsの使い方

Kotlinではcontractという仕組みを使って、コンパイラーとの約束ができます。
この仕組みが使われている、最も身近な例の一つが`isNullOrEmpty()`でしょう。
以下のように`isNullOrEmpty()` がfalseを返した場合、そのレシーバーはNonNullとして判断され、スマートキャストされます。

```kotlin
if (!a.isNullOrEmpty()) {
    a.get(0)
}
```

例えば、`isNullOrEmpty()`を独自実装すると以下のようになると思います。

```kotlin
inline fun <T> Collection<T>?.isNullOrEmpty(): Boolean =
    this == null || this.isEmpty()
```

しかし、このメソッドを使った場合は、スマートキャストしてくれません。
コンパイラーが`isNullOrEmpty()`の判定と、レシーバーがnullか否かの関係を知らないからです。

`isNullOrEmpty()`の実装は以下のようになっています。

```kotlin
@SinceKotlin("1.3")
@kotlin.internal.InlineOnly
public inline fun <T> Collection<T>?.isNullOrEmpty(): Boolean {
    contract {
        returns(false) implies (this@isNullOrEmpty != null)
    }

    return this == null || this.isEmpty()
}
```

ここで、contractの構文が使われています。
DSLが使われており、contractの仕組みを知らなくてもなんとなく意味が分かると思います。
このメソッドがfalseを返した場合は、レシーバがnullでないことを意味する。みたいなことが書かれているみたいですね。

# kotlin.contractsの使い方

kotlin.contractsはSDKの中でしか使えない機能というわけでは無く、ユーザープログラム上で使うこともできます。
ただし、

```kotlin
@OptIn(ExperimentalContracts::class)
```

のアノテーションが必要です。まだ実験的な機能ですので、将来的に使えなくなったり仕様が変化する可能性があります。

:::note warn
強力な反面リスクも高い機能です。使う場合は慎重に確認しましょう。
:::

## 戻り値とレシーバ・引数の型情報を伝える

kotlin.contractsでは、拡張関数のレシーバ、もしくは引数の型情報を伝えることができます。

レシーバを使う場合、以下のように定義し

```kotlin
@OptIn(ExperimentalContracts::class)
private fun Any?.isNotNull(): Boolean {
    contract {
        returns(true) implies (this@isNotNull != null)
    }
    return this != null
}
```

以下のように使うことで、戻り値がtrueの時、レシーバがNonNullとして扱われます

```kotlin
if (a.isNotNull()) {
    a.length
}
```

引数の情報を返すなら以下のように定義します。

```kotlin
@OptIn(ExperimentalContracts::class)
private fun isNotNull(value: Any?): Boolean {
    contract {
        returns(true) implies (value != null)
    }
    return value != null
}
```

`returns()`では戻り値が、true/false/nullの場合の条件を、`returnsNotNull()`であれば戻り値がNonNullの場合の条件を指定することができます。

例えば、以下のように使い、戻り値がNonNullの場合、レシーバーもNonNullであると伝えることができます。

```kotlin
@OptIn(ExperimentalContracts::class)
private fun Any?.toInt(): Int? {
    contract {
        returnsNotNull() implies (this@toInt != null)
    }
    return this as? Int
}
```

戻り値の条件は、true/false/null/NonNullだけで、それ以外の条件、例えば、戻り値が0なら〜のような条件は指定できません。

よく考えれば当然ですが、誤解しやすそうな点として、以下のように戻り値がNullであると伝えた場合、

```kotlin
@OptIn(ExperimentalContracts::class)
private fun Any?.isNull(): Boolean {
    contract {
        returns(true) implies (this@isNull == null)
    }
    return this == null
}
```

以下のように、否定がNonNullであるとは判定してくれません。

```kotlin
if (!a.isNull()) {
    a.length
}
```

この条件の時にnullである。ということは、それ以外にnullとなる条件がない、という意味とは限らないため、否定=NonNullと判定できないからです。
NonNull判定が必要であれば、NonNullの条件を伝える必要があります。

```kotlin
@OptIn(ExperimentalContracts::class)
private fun Any?.isNull(): Boolean {
    contract {
        returns(false) implies (this@isNull != null)
    }
    return this == null
}
```

Nullable以外に、型情報を伝えることもできます。
例えば、以下のように定義することで、

```kotlin
sealed interface Foo {
    data class Valid(val value: Int) : Foo
    data object Invalid: Foo
}

@OptIn(ExperimentalContracts::class)
fun Foo.isValid(): Boolean {
    contract {
        returns(true) implies (this@isValid is Foo.Valid)
    }
    return this is Foo.Valid
}
```

以下のように、型判定したのと同様に、スマートキャストが使えるようになります。

```kotlin
if (foo.isValid()) {
    foo.value // Validにスマートキャストされている
```


## ラムダの実行回数を伝える

`callsInPlace`でラムダの実行回数を伝えることができます。

例えば、以下のようにラムダを実行するメソッドがあったとして、

```kotlin
private inline fun run(block: () -> Unit) {
    block()
}
```

以下の記述は、通常コンパイルエラーとなります。

```kotlin
val a: String?
run {
    a = "Hello"
}
```

`a`は`val`宣言されているため、初期値の指定として1回しか代入が許されません。しかし、`block`が何回実行されるのか、必ず実行されるのかも分からないため、この記述を許可して良いのかをコンパイラーが判断できないからです。

`contract`を使って以下のように記述すると、`block`が1回だけ必ず実行されることを約束できます。

```kotlin
@OptIn(ExperimentalContracts::class)
private inline fun run(block: () -> Unit) {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
}
```

これにより、以下の記述はコンパイルできるようになります。

```kotlin
val a: String?
run {
    a = "Hello"
}
```

実行回数については、以下のような条件が指定可能です。

|指定|意味|
|--|--|
|AT_MOST_ONCE|最大でも1回実行される|
|AT_LEAST_ONCE|最低でも1回実行される|
|EXACTLY_ONCE|必ず1回実行される|
|UNKNOWN|実行回数は不明|

---

**「このメソッドの中でNullチェックしているから、ここではNonNullであることは確実なのにスマートキャストしてくれない。not-null assertion operator (!!) は使いたくないし、無駄に再チェックも書きたくない」**
というのはKotlin使っていてよくあるシーンですが、kotlin.contractsを使うことで、少し便利に書けるようになるかもしれません。

以上です。