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.
0 comentarios