Kotlin契約
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 の呼び出しで例外がスローされない場合、プログラマーは誰でもこのコードを読み取って、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. 戻り値に基づく保証の作成
ここでは、ターゲット関数が戻った場合にターゲット条件が満たされることを指定します。これはモチベーションセクションで使用しました。
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からで入手できます。