Contrats Kotlin

Contrats Kotlin

1. Vue d'ensemble

Dans ce tutoriel, nous parlerons deKotlin Contracts. Leur syntaxe n'est pas encore stable, mais l'implémentation binaire l'est, et Kotlinstdlib e trouve déjà en train de les utiliser.

Fondamentalement, les contrats Kotlin sont un moyen d'informer le compilateur du comportement d'une fonction.

2. Maven Setup

Cette fonctionnalité est introduite dans Kotlin 1.3, nous devons donc utiliser cette version ou une version plus récente. Pour ce didacticiel,we’ll use the latest version available - 1.3.10.

Veuillez consulter nosintroduction to Kotlin pour plus de détails sur la configuration de cela.

3. Motivation pour les contrats

Aussi intelligent que soit le compilateur, il n’arrive pas toujours à la meilleure conclusion.

Considérons l'exemple ci-dessous:

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")
    }
}

Tout programmeur peut lire ce code et savoir querequest n’est pasnull si un appel àvalidate ne lève pas d’exception. In other words, it’s impossible for our println instruction to throw a NullPointerException.

Malheureusement, le compilateur n’en est pas conscient et ne nous permet pas de référencerrequest.arg.

Cependant, nous pouvons améliorervalidate par un contrat qui définit que si la fonction renvoie avec succès - c'est-à-dire qu'elle ne lève pas d'exception - alors l'argument donné n'est pasnull:

@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")
    }
}

Voyons ensuite cette fonctionnalité plus en détail.

4. L'API des contrats

Le formulaire de contrat général est:

function {
    contract {
        Effect
    }
}

Nous pouvons lire cela comme «invoquer la fonction produit l'effet».

Dans les sections suivantes, examinons les types d’effets pris en charge actuellement par le langage.

4.1. Faire des garanties en fonction de la valeur de retour

Here we specify that if the target function returns, the target condition is satisfied. Nous l'avons utilisé dans la sectionMotivation.

Nous pouvons également spécifier une valeur dans lereturns - qui indiquerait au compilateur Kotlin que la condition n'est remplie que si la valeur cible est renvoyée:

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
}

Cela aide le compilateur à faire un cast intelligent dans la fonctionprocessEvent.

Notez que, pour l'instant, les contratsreturns n'autorisent quetrue,false etnull sur le côté droit de implies.

Et même siimplies prend un argumentBoolean, seul un sous-ensemble d'expressions Kotlin valides est accepté: à savoir,null-checks (== null,! = null ), vérifications d'instance (is,!is), opérateurs logiques (&&,||,!).

Il existe également une variante qui cible toute valeur renvoyée non -null:

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

4.2. Faire des garanties sur l'utilisation d'une fonction

Le contratcallsInPlace exprime les garanties suivantes:

  • lescallable ne seront pas appelés une fois la fonction propriétaire terminée

  • il ne sera pas non plus transmis à une autre fonction sans le contrat

Cela nous aide dans les situations suivantes:

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
}

Nous pouvons corriger les erreurs en aidant le compilateur à s'assurer que le bloc donné est garanti pour être appelé et appelé une seule fois:

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

Les fonctions standard de l'utilitaire Kotlinrun,with,apply, etc. définissent déjà de tels contrats.

Ici, nous avons utiliséInvocationKind.EXACTLY_ONCE. Other options are AT_LEAST_ONCEAT_MOST_ONCE, and UNKNOWN.

5. Limitations des contrats

Alors que les contrats Kotlin semblent prometteurs,the current syntax is unstable at the moment, and it’s possible that it will be completely changed in the future.

En outre, ils ont quelques limitations:

  • Nous ne pouvons appliquer des contrats que sur des fonctions de niveau supérieur avec un organisme, c'est-à-dire nous ne pouvons pas les utiliser sur les champs et les fonctions de classe.

  • L'appel de contrat doit être la première déclaration dans le corps de la fonction.

  • Le compilateur approuve les contrats sans condition; cela signifie que le programmeur est responsable de la rédaction de contrats corrects et sonores. A future version may implement verification.

Enfin, les descriptions de contrat ne permettent que des références à des paramètres. Par exemple, le code ci-dessous ne compile pas:

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. Conclusion

La fonctionnalité semble plutôt intéressante et même si sa syntaxe est au stade du prototype, la représentation binaire est suffisamment stable et fait déjà partie destdlib. Cela ne changera pas sans un cycle de migration harmonieux, ce qui signifie que nous pouvons dépendre d'artefacts binaires avec des contrats (par exemple, stdlib) pour avoir toutes les garanties de compatibilité habituelles.

That’s why our recommendation is that it’s worth using contracts even now - il ne serait pas trop difficile de changer les déclarations de contrat si et quand leur DSL change.

Comme d'habitude, le code source utilisé dans cet article est disponibleover on GitHub.