コトリンの契約
1概要
このチュートリアルでは、https://kotlinlang.org/docs/reference/whatsnew13.html#contracts[Kotlin Contracts]について説明します。それらの構文はまだ安定していません、しかしバイナリ実装はそうです、そして、Kotlin
__stdlib
__それ自身はすでにそれらを使用するようにしています。
-
基本的に、Kotlin規約は、関数の動作についてコンパイラに通知する方法です。
2 Mavenのセットアップ
この機能はKotlin 1.3で導入されたので、このバージョンかそれより新しいものを使う必要があります。このチュートリアルでは、https://search.maven.org/classic/#search%7Cgav%7C1%7Cg%3A%22org.jetbrains.kotlin%22%20AND%20a%3A%22kotlin-stdlib%22[we’ll入手可能な最新版を使用する] – 1.3.10。
設定の詳細については、https://www.baeldung.com/kotlin[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
を呼び出しても例外がスローされない場合、すべてのプログラマがこのコードを読んで
request
が
null
ではないことを知ることができます。言い換えれば、
__ 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. 戻り値に基づいて保証する
-
ここでは、target関数が戻ったときにtarget条件が満たされるように指定しています。
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
関数でスマートキャストを行うことができます。
-
現時点では、
return
規約では、
の右側にある
true
、
false
、および
null__のみが許可されています。
implies
は
Boolean
引数を取りますが、有効なKotlin式のサブセットのみが受け入れられます。つまり、
null
-checks(
== null
、
__!
= null)、instance-checks(
is
、
!is
)、論理演算子(
null
以外の戻り値をターゲットにしたバリエーションもあります。
contract {
returnsNotNull() implies (event is MyEvent)
}
** 4.2. 機能の使用について保証する
callsInPlace
契約は、以下の保証を表します。
-
callable
はowner-functionが終了した後には呼び出されません -
契約なしに他の機能に渡すこともできません。
これは以下のような状況で私たちを助けます:
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
}
与えられたブロックが確実に呼び出され、一度だけ呼び出されることを保証するようにコンパイラに手助けすることでエラーを修正できます。
@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が変更されても、契約宣言を変更するのはそれほど困難ではないでしょう。
いつものように、この記事で使われているソースコードはhttps://github.com/eugenp/tutorials/tree/master/core-kotlin[GitHubで利用可能]です。