Contratos Kotlin
1. Visão geral
Neste tutorial, falaremos sobreKotlin Contracts. Sua sintaxe ainda não é estável, mas a implementação binária sim, e Kotlinstdlib já está colocando-os em uso.
Basicamente, os contratos Kotlin são uma forma de informar o compilador sobre o comportamento de uma função.
2. Configuração do Maven
Esse recurso foi introduzido no Kotlin 1.3, portanto, precisamos usar esta versão ou uma versão mais recente. Para este tutorial,we’ll use the latest version available - 1.3.10.
Consulte nossointroduction to Kotlin para obter mais detalhes sobre a configuração.
3. Motivação para contratos
Por mais inteligente que seja o compilador, ele nem sempre chega à melhor conclusão.
Considere o exemplo abaixo:
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")
}
}
Qualquer programador pode ler este código e saber querequest não énull se uma chamada paravalidate não lançar uma exceção. In other words, it’s impossible for our println instruction to throw a NullPointerException.
Infelizmente, o compilador não está ciente disso e não nos permite fazer referência arequest.arg.
No entanto, podemos aprimorarvalidate por um contrato que define que se a função retornar com sucesso - ou seja, ela não lança uma exceção - então o argumento fornecido não énull:
@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")
}
}
A seguir, vamos dar uma olhada neste recurso com mais detalhes.
4. A API de contratos
O formulário geral do contrato é:
function {
contract {
Effect
}
}
Podemos ler isso como “invocar a função produz o efeito”.
Nas seções a seguir, vamos dar uma olhada nos tipos de efeitos que a linguagem suporta agora.
4.1. Fazendo garantias com base no valor de retorno
Here we specify that if the target function returns, the target condition is satisfied. Usamos isso na seçãoMotivation.
Também podemos especificar um valor emreturns - isso instruiria o compilador Kotlin de que a condição será atendida somente se o valor de destino for retornado:
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
}
Isso ajuda o compilador a fazer um elenco inteligente na funçãoprocessEvent.
Observe que, por enquanto, os contratos dereturns permitem apenastrue,false enull no lado direito de implies.
E emboraimplies receba um argumentoBoolean, apenas um subconjunto de expressões Kotlin válidas é aceito: a saber,null-checks (== null,! = null ), verificações de instância (is,!is), operadores lógicos (&&,||,!).
Há também uma variação que visa qualquer valor retornado diferente denull:
contract {
returnsNotNull() implies (event is MyEvent)
}
4.2. Fazendo garantias sobre o uso de uma função
O contratocallsInPlace expressa as seguintes garantias:
-
ocallable não será invocado depois que a função do proprietário for concluída
-
também não será passado para outra função sem o contrato
Isso nos ajuda em situações como abaixo:
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
}
Podemos corrigir os erros ajudando o compilador a garantir que o bloco fornecido seja chamado e chamado apenas uma vez:
@ExperimentalContracts
inline fun myRun(block: () -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}
As funções de utilidade padrão do Kotlinrun,with,apply, etc. já definem esses contratos.
Aqui usamosInvocationKind.EXACTLY_ONCE. Other options are AT_LEAST_ONCE, AT_MOST_ONCE, and UNKNOWN.
5. Limitações de contratos
Embora os contratos Kotlin pareçam promissores,the current syntax is unstable at the moment, and it’s possible that it will be completely changed in the future.
Além disso, eles têm algumas limitações:
-
Só podemos aplicar contratos a funções de nível superior com um organismo, ou seja, não podemos usá-los em campos e funções de classe.
-
A chamada do contrato deve ser a primeira instrução no corpo da função.
-
O compilador confia nos contratos incondicionalmente; isso significa que o programador é responsável por escrever contratos corretos e corretos. A future version may implement verification.
E, finalmente, as descrições dos contratos permitem apenas referências a parâmetros. Por exemplo, o código abaixo não compila:
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. Conclusão
O recurso parece bastante interessante e mesmo que sua sintaxe esteja no estágio de protótipo, a representação binária é estável o suficiente e já faz parte destdlib. Não mudará sem um ciclo de migração harmonioso, e isso significa que podemos depender de artefatos binários com contratos (por exemplo, stdlib) para ter todas as garantias de compatibilidade usuais.
That’s why our recommendation is that it’s worth using contracts even now - não seria muito difícil alterar as declarações de contrato se e quando seu DSL mudasse.
Como de costume, o código-fonte usado neste artigo está disponívelover on GitHub.