Contracts en Kotlin: Haz más listo al compilador
Antonio Leiva

El compilador de Kotlin es muy potente, y nos puede ayudar en muchos aspectos en los que otros compiladores como Java pasan de largo.

Temas como los nulos, inferencia de tipos, genéricos, smart casting, y un largo etcétera, hacen del compilador de Kotlin una herramienta muy potente.

Pero hay puntos que el compilador no puede inferir, y ahí es donde los contracts o contratos de Kotlin entran en juego.

¿Cómo se si, tras cierta llamada, un valor sigue siendo nulo o no? ¿O cómo valido que si se cumple, entonces cierta variable se puede castear automáticamente a un tipo?

¿Qué son los contracts?

Los contratos tienen una estructura del siguiente tipo:

fun myFun(){
    contract {
        Efecto
    }
}

Ese efecto le va a dar al compilador información para hacerlo más inteligente en todo lo que ocurra posteriormente a la llamada de esa función.

Estos nos van a permitir garantizar que ciertas condiciones se cumplen, y por tanto el compilador puede darlas por hecho y así permitirnos hacer cosas que de otro modo no se podrían

Garantías sobre el valor de retorno

Podemos definir que, dado un valor de retorno, se cumple una condición.

Veamos este ejemplo:

val name: String? = ...

if (!name.isNullOrEmpty()){
    name.reversed()
}

¿Cómo puede ser que pueda llamar a la función reversed() si name podría ser null?

La magia ocurre dentro de la función isNullOrEmpty():

public inline fun CharSequence?.isNullOrEmpty(): Boolean {
    contract {
        returns(false) implies (this@isNullOrEmpty != null)
    }

    return this == null || this.length == 0
}

¿Ves el bloque contract? En él se indica que si la función devuelve false, entonces el String que ha llamado a la función no es nulo.

Por tanto el compilador hace un smart cast al tipo no nulo, y ya no hace falta comprobar la nulidad dentro del if.

Pero también podemos hacer smart casts a tipos concretos. Por ejemplo, imagina que quieres una función que compruebe si una View de Android tiene hijas.

En Android, las vistas de tipo ViewGroup tienen hijos. Podríamos hacer algo así:

fun View.hasChildren(): Boolean {
    contract {
        returns(true) implies (this@hasChildren is ViewGroup)
    }

    return (this is ViewGroup)
}

Al usar ahora esta función en un if, el smart cast a ViewGroup se hace automático, y puede llamar a su property childCount:

fun children(view: View) {
    if(view.hasChildren()){
        println(view.childCount)
    }
}

Incluso puedes crear funciones de aserción, que lancen una excepción si una condición no se cumple, pero que hagan smart cast si se cumplen:

fun View.assertHasChildren() {
    contract {
        returns() implies (this@assertHasChildren is ViewGroup)
    }

    if(this !is ViewGroup) throw IllegalStateException()
}

Ahora se podría usar así:

fun children(view: View) {
    view.assertHasChildren()
    println(view.childCount)
}

Es posible que hayas usado funciones de Kotlin similares, como requireNotNull. El contrato que usan es muy parecido al anterior.

Garantías en el número de llamadas a una lambda

Si una función recibe una lambda, identificar al compilador cuántas veces se puede llamar a esa lambda le va a hacer ser más inteligente al respecto.

Por ejemplo, cuando usas la función let, puedes hacer esto:

data class Person(val name: String, val age: Int)

fun firstLetter(person: Person): Char {
    val firstLetter: Char
    person.name.let {
        firstLetter = it[0]
    }
    return firstLetter
}

O incluso esto:

fun firstLetter(person: Person): Char {
    person.name.let {
        return it[0]
    }
}

De primeras quizá no veas algo raro, pero si la función que se pasa a let se ejecutara dentro varias veces, esa asignación a firstLetter o ese return darían problemas.

firstLetter es val (solo se le puede asignar un valor), el compilador no debería dejarme asignar un valor varias veces.

De hecho, nadie me dice que esa función a lo mejor no se llame nunca, en función del código de let. ¿Y entonces esa función no retorna nunca un valor?

De forma normal, el compilador daría error aquí. Si yo me creo mi propia función let:

inline fun <T, R> T.let2(block: (T) -> R) {
    block(this)
}

Y la llamo, tendré este error:

Porque no puede garantizar que la lambda que se le pasa a let2 se vaya a llamar, y por tanto que firstLetter vaya a devolver algo.

¿Pero y si hubiera una forma de decirle que por contrato, la función se va a llamar al menos una vez? Entonces sí que podríamos garantizar el valor de retorno:

inline fun String.let2(block: (String) -> Char): Char {
    contract {
        callsInPlace(block, InvocationKind.AT_LEAST_ONCE)
    }
    return block(this)
}

Pero esto no nos asegura que se vaya a llamar solo una vez, por tanto este código seguirá dando error:

¿Cómo solucionarlo? Fácil, asegurando que esa invocación va a ocurrir una y solo una vez:

    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }

Aún experimental

Esta feature fue publicada por primera vez con Kotlin 1.3 como experimental, y desde entonces no ha cambiado su estado.

Además, el IDE no termina de enterarse muy bien de los cambios, y para estar seguros hay que invalidar cachés cada poco tiempo.

¿Merece la pena usarlo? Yo diría que si tienes una serie de funciones de utilidad o una librería que va a ser extraída y después usada en distintos proyectos, merece la pena el esfuerzo para simplificar el código del que la usa.

Si no, está bien para funciones que vayas a usar mucho y no vayas a cambiar demasiado. De lo contrario, el IDE no va a estar de tu parte para ayudarte a detectar los fallos en tiempo real. Tendrás que invalidar cada poco hasta que tus funciones sean estables.

Quizá también te interese…

Varianza en Kotlin – Gana la batalla a los genéricos

Varianza en Kotlin – Gana la batalla a los genéricos

Cuando hablamos de herencia, normalmente tenemos claro que cuando una clase hereda de otra, podemos usar la clase más genérica para dar soluciones más flexibles. Por tanto, si tengo los tipos Int y Long, puedo crearme una función que reciba Number y dar una solución...

Kotlin 1.5.0 : Las 5 novedades que puedes empezar a usar hoy

Kotlin 1.5.0 : Las 5 novedades que puedes empezar a usar hoy

Kotlin 1.5.0 ya está aquí, y como siempre trae una serie de novedades que te van a interesar muchísimo. Cabe destacar que a partir de ahora, de acuerdo las nuevas versiones de Kotlin se lanzarán cada 6 meses, independientemente de las nuevas funcionalidades que...

¿Qué es Kotlin y para qué sirve?

¿Qué es Kotlin y para qué sirve?

Kotlin es un lenguaje de programación de código abierto creado por JetBrains que se ha popularizado gracias a que se puede utilizar para programar aplicaciones Android. Pero si has llegado hasta aquí pensando que Kotlin solo se puede usar en Android, lo que te voy a...

0 comentarios

Enviar un comentario

Los datos personales que proporciones a través de este formulario quedarán registrados en un fichero de Antonio Leiva Gordillo, con el fin de gestionar los comentarios que realizas en este blog. La legitimación se realiza a través del consentimiento de la parte interesada. Si no se acepta, no podrás comentar en este blog. Los datos que proporciona solo se utilizan para evitar el correo no deseado y no se usarán para nada más. Puede ejercer los derechos de acceso, rectificación, cancelación y oposición en contacto@devexperto.com.

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Acepto la política de privacidad *