Hoy, tras una larga cadena de actualizaciones incrementales para la rama 1.2.X, ha llegado la hora de ver qué nos espera con Kotlin 1.3. Nos complace anunciar la primera versión preeliminar de la nueva versión mayor: Kotlin 1.3-M1.

Kotlin 1.3 trae diversas mejoras incluyendo la promoción de las corrutinas, nueva aritmética sin signo experimental y mucho más. Podéis seguir leyendo para más detalles.

En primer lugar, nos gustaría agradecer a los contribuyentes externos cuyas pull requsts y commits se han incluído en esta versión: Raluca Sauciuc, Toshiaki Kameyama, Leonardo Lopes, Jake Wharton, Jeff Wright, Lucas Smaira, Mon_chi, Nico Mandery, Oskar Drozda.

La lista completa de cambios de esta versión la podéis encontrar en el changelog.

Las Corrutinas ya están marcadas como estables

Por fin, las corrutinas ya no son experimentales en la 1.3. Hemos estabilizado tanto la sintaxis como la el API de la Librería estándar y serán retrocompatibles en el futuro.

Las corrutinas se han mejorado sustancialmente desde su introducción en la 1.1. Las actualizaciones más notables incluyen:

  • KT-16908 Soporte para callable references para funciones suspendibles
  • KT-18559 Serializabilidad de todas las clases relacionadas con las corrutinas

Hemos simplificado las APIs de bajo nivel y las hemos sacado del paquete experimental.

También estamos trabajando en una versión multiplataforma de las APIs de corrutinas, incluyendo soporte para iOS a través de Kotlin/Native.

Migración a las nuevas corrutinas

Como hemos dicho antes, todas las funciones relativas a las corrutinas de Kotlin han perdido la parte experimental de sus nombres de paquete, y hemos movido de forma permanente las funciones buildSequence y buildIterator al paquete kotlin.sequences.

Por lo que respecta a la sintaxis, sigue habiendo un modificador suspend para soportar las corrutinas, y todas las reglas para trabajar con corrutinas se mantienen igual que en su versión experimental.

Hemos simplificado la interfaz de Continuation<T>. Ahora solo tiene un único método miembro resumeWith(result: SuccessOrFailure<T>). Y hemos pasado a métodos de extensión tanto resume(value: T) como resumeWithException(exception: Throwable).
Esto debería afectar únicamente a una cantidad pequeña de código que defina sus propios builders de corrutinas. Por ejemplo, una posible definición para la función suspendible await() para la clase CompletableFuture<T> podría ser algo como:

suspend fun <T> CompletableFuture<T>.await(): T = suspendCoroutine { cont ->
    whenComplete { value, exception ->
        when {
            exception != null -> cont.resumeWithException(exception)
            else -> cont.resume(value)
        }
    }
}

Las corrutinas graduadas utilizan una interfaz binaria diferente (donde la mayor diferencia es el nombre de paquete) y no son compatibles a nivel binario con las corrutinas experimentales. Para asegurar una migración suave, añadiremos una capa de compatibilidad en la versión 1.3 y las clases experimentales permanecerán en la librería estándar. De forma que el código compilado con Kotlin/JVM que usase las corrutinas experimentales de la 1.1 o la 1.2 seguirá funcionando con Kotlin 1.3.

Sin embargo, en la 1.3-M1 no proporcionamos soporte para invocar funciones suspendibles experimentales desde código compilado con la versión 1.3 del lenguaje. Para usar librerías que expongan funciones suspendibles, tendréis que usar versiones de dichas librerías compiladas para Kotlin 1.3. Un pequeño inconveniente que solucionaremos pronto.

Por nuestra parte, para cada versión que saquemos de kotlinx.coroutines, vamos a proporcionar una versión x.x.x-eap13 compilada con Kotlin 1.3, sin el paquete experimental mientras estamos en la fase de preview.

El IDE os asistirá en la migración a la nueva API de corrutinas, e iremos mejorando el rango de escenarios de migración antes de la 1.3.

Nuevas características

Mientras que las características más grandes de la 1.3 serán experimentales (más información abajo), vamos ya a incluir algunas pequeñas mejoras para vuestra conveniencia.

Capturando el sujeto de los when en una variable

Ahora el lenguaje soporta el típico caso de capturar el sujeto de una expresión de un when en una variable:

fun Request.getBody() =
    when (val response = executeRequest()) {
        is Success -> response.body
        is HttpError -> throw HttpException(response.status)
    }

Esta pequeña mejora hace que la variable response esté en el ámbito correcto dentro del when y hace que el flujo de la lógica sea más orientado a expresiones.

Esta era una de las peticiones de mejoras más populares de nuestro gestor de tareas. ¡Gracias a todos los que habéis votado por vuestro feedback!

Soporte de @JvmStatic y @JvmField en los companions de interfaces

Con esta mejora, vuestras interfaces ahora Kotlin pueden exponer miembros estáticos (tanto campos como métodos) a clientes Java. La sintaxis es similar a la que habría en clases normales; un companion object de una interfaz ahora puede tenerr @JvmStatic o @JvmField dentro:

interface Service {
    companion object {
        @JvmField
        val ID = 0xF0EF

        @JvmStatic
        fun create(): Service = ...
    }
}

Nótese que la anotación @JvmField es un todo o nada: para usarlo en un companion de interfaces, todos los campos definidos tienen que ser públicos y estar debidamente anotados con @JvmField.

Para utilizar la anotación @JvmStatic en miembros de los companions de las interfaces, la opción de compilación -jvm-target debe ser al menos 1.8.

Declaraciones anidadas en clases de anotación

Antes de la 1.3, las clases de anotación no podían tener cuerpo en Kotlin. Esta versión relaja dicha limitación, permitiendo que tengan clases anidadas, interfaces y objetos incluyendo un companion object. De esta forma, ahora es posible anidar una anotación dentro de otra:

annotation class Outer(val param: Inner) {
    annotation class Inner(val value: String)
}

Ya que soportamos @JvmField y @JvmStatic en interfaces, ahora también es posible declarar dichos miembros en los companion objects de las anotaciones:

annotation class Annotation {
    companion object {
        @JvmField
        val timestamp = System.currentTimeMillis()
    }
}

Tipos funcionales de aridades grandes

¡Ahora los tipos funcionales pueden tener más de 22 parámetros! Hemos subido este límite a 255 parámetros, que es el número máximo de parámetros que puede tener la JVM en la práctica. Para averiguar cómo hemos logrado hacer esto sin introducir otras 233 clases, echad un ojo al KEEP correspondiente.

Características del lenguaje experimentales

Como ha quedado demostrado con las corrutinas, proporcionar acceso anticipado a características grandes marcándolas como experimental nos ayuda a obtener un feedback valiosísimo de la comunidad. Vamos a seguir utilizando esta técnica para asegurarnos de que Kotlin solo acaba incluyendo elecciones de diseño muy testeadas y diseñadas para durar.

Kotlin 1.3 trae tres nuevas características muy interesantes que vamos a incluir como experimentales por el momento. Y además requerirán on opt-in explícito para poderlas usar en vuestros proyectos. Sin el opt-in, los usos de la característica estarán marcados como advertencias o errores.

Clases inline

Las clases inline permiten envolver un valor de un cierto tipo sin crear un objeto envoltorio.

inline class Name(internal val value: String)

Cuando se utiliza dicha clase, el compilador empotra el contenido y las operaciones sobre el valor envuelto directamente. De forma que:

val name: Name = Name("Mike")
fun capitalize(name: Name): Name = Name(name.value.capitalize())

resulta en el mismo código compilado que:

val name: String = "Mike"
fun capitalize(name: String): String = name.capitalize()

En cierta medida las clases inline actúan de forma similar a los type aliases, pero no son compatibles a nivel de asignación con el tipo que envuelven, así que no puedes asignar una variable de tipo String a una de tipo Name ni al revés.

Como las clases inline no tienen identidad, no se puede usar el operador === sobre ellas.

Aún con todo, siguen habiendo casos en los que las clases inline acaban con una instancia creada, de forma similar a cómo la primitiva Int acaba boxeada en determinados casos ("auto-boxing"):

val key: Any = Name("Mike") // boxing to actual Name wrapper

val pair = Name("Mike") to 27 // Pair is a generic type, so Name is boxed here too

Esta característica se puede habiliar con la opción de compilación -XXLanguage:+InlineClasses.

Echad un ojo al KEEP para más detalles. Y aseguraos de leer la sección de "Limitaciones".

Tipos sin signo

Una de las aplicaciones más obvias para las clases inline, son los tipos sin signo. La librería estándar tiene ahora UInt, ULong, UByte y UShort definidas como clases inline envolviendo sus contrapartidas con signo. Dichas clases definen sus propias operaciones aritméticas que interpretan el valor almacenado como si fuesen enteros sin signo.

val operand1 = 42
val operand2 = 1000 * 100_000

val signed: Int = operand1 * operand2
val unsigned: UInt = operand1.toUInt() * operand2.toUInt()

Además de los tipos nuevos, hay nuevas características del lenguaje para soportar estos tipos, que hacen uso especial de estos tipos.

  • Se permiten varags de tipos sin signo, a diferencia de otras clases inline:
    fun maxOf(vararg values: UInt): UInt { ... }
    
  • Hemos añadido soporte para literales sin signo (con el sufijo u):
    val uintMask = 0xFFFF_FFFFu
    val ulongUpperPartMask = 0xFFFF_FFFF_0000_0000uL
    
  • Permitimos valores constantes de tipos sin signo:
    const val MAX_SIZE = 32768u
    

Nótese que en la 1.3-M1 todavía no permitimos expresiones constantes complejas sobre tipos sin signo:

const val MAX_SIZE_BITS = MAX_SIZE * 8u // Error in 1.3-M1

Pero tenemos intención de soportarlas más adelante.

Soportamos los tipos sin signo como una API experimental: tenéis que optar explícitamente a usarlas, o de lo contrario se marcará una advertencia donde lo uséis. Para optar:

  • Podéis o bien anotar los bloques de código que utilizan los tipos sin signo con la anotación @UseExperimental(ExperimentalUnsignedTypes::class)
  • O bien especificar la opción de compilación -Xuse-experimental=kotlin.ExperimentalUnsignedTypes

Además, si sois autores de librerías, y vais a usar los tipos sin signo experimentales, os recomendamos que anotéis vuestras APIs con la anotación @ExperimentalUnsignedTypes. Esto propagará el grado de experimentalidad de los tipos sin signo a los usuarios de vuestras librerías.

Para más información sobre los tipos sin signo, podéis mirar su KEEP.

Anotaciones para marcar APIs como experimentales y optar por usarlas

El mecanismo usado para marcar los tipos sin signo como experimentales mencionado arriba, está también disponible de forma genérica para los autores de librerías. Esto os permitirá proporcionar APIs experimentales (por ejemplo aquellas que están sujetas a cambios en el futuro), que pueden romperse en cualquier momento y pueden requerir ajustar cosas y recompilar.

Los usuarios de dichas APIs experimentales pueden optar a usarlas básicamente aceptando algo así como "Comprendo que estoy usando una declaración experimental que puede cambiar en cualquier momento". De forma que sin un consentimiento explícito por el usuario, se marcará los usos como advertencias o errores.

El objetivo en última instancia es permitir a los autores de librerías liberar sus APIs antes que de normal y con más frecuencia sin el compromiso de compatibilidad hacia atrás (ni de código ni binaria).

La API de anotaciones experimentales está descrita en detalle en este KEEP.

Nuevas APIs de la librería estándar

Ahora echemos un ojo a qué APIs nuevas hay disponibles en esta versión.

SuccessOrFailure

La clase inline SuccessOrFailure es a efectos prácticos un tipo de unión discriminante entre éxito y fallo del resultado de ejecución de una función de Kotlin: Success T | Failure Throwable.

Se ha introducido para permitir capturar el resultado de la jecución de una función, bien haya ido bien o no, de forma que se pueda procesar más tarde.

val files = listOf(File("a.txt"), File("b.txt"), File("42.txt"))
val contents: List<SuccessOrFailure<String>> = files.map { runCatching { readFileData(it) } }

println("map successful items:")
val upperCaseContents: List<SuccessOrFailure<String>> = 
    contents.map { r -> r.map { it.toUpperCase() } } 
upperCaseContents.printResults()

println()
println("map successful items catching error from transform operation:")
val intContents: List<SuccessOrFailure<Int>> = 
    contents.map { r -> r.mapCatching { it.toInt() } }
intContents.printResults()

El principal motivo para introducir esta clase es la nueva interfaz de Continuation, donde queremos tener una única función resumeWith(result: SuccessOrFailure<T>) en vez de dos, resume(T) y resumeWithException(Throwable).

Nótese que la guía de estilo de Kotlin desaconseja usar SuccessOrFailure como valor de retorno para funciones de Kotlin. De forma similar a cómo se aconseja que los usuarios tengan funciones suspend devolviendo un valor concreto, en vez de tener funciones normales devolviendo un Deferred de dicho tipo.

Hay ciertas excepciones para dicha regla, que están explicadas con más detalle en la propuesta KEEP-127 (haced scroll hasta la sección de estilo y excepciones).

Generadores de números aleatorios multiplataforma

Mientras que no suele haber problemas en generar números aleatorios en Kotlin/JVM (al margen de ThreadLocalRandom que no está disponible antes del JDK7), no había un API uniforme todavía para hacer eso en otras plataformas de Kotlin, y más todavía en código multiplataforma.

Esta versión introduce una nueva clase abstracta Random que sirve como clase base para las implementaciones de generadores de números aleatorios.

Nótese que si únicamente necesitas un número aleatorio, no hay necesidad de heredar e implementar esta clase. Hay un generador de números aleatorio por defecto a mano siempre: el companion object de Random. De forma que obtener un número aleatorio es tan fácil como:

val number = Random.nextInt(limit)  // number is in range [0, limit)

Se puede conseguir un generador de números aleatorio repetible con una semilla concreta utilizando la función libre Random(seed):

val seriesRng = Random(42)
val series = List(100) { seriesRng.nextDouble() }

Para más detalles mirad el KEEP-131.

Companion object para el tipo Boolean

Los tipos básicos de Kotlin como Int, Char y String tienen un companion object con ciertas propiedades como MIN_VALUE.

Hasta ahora, el tipo Boolean iba a parte sin incluir ningún companion object.

Los miembros de nuestra comunidad nos han convencido de que hay casos de uso para tener un companion object para Boolean aún sin ningún tipo de propiedad, y Leonardo Lopes ha ayudado con su implementación.

Nuevas constantes en los companion object de los tipos básicos

Hemos inclúido nuevas propiedades a los companion objects de los tipos básicos:

  • Byte, Short, Int, Long y Char tienen ahora las constantes SIZE_BITS y SIZE_BYTES, que indican cuántos bits o bytes tiene un cierto tipo en su forma binaria.
  • Char tiene ahora MIN_VALUE y MAX_VALUE correspondiendo a \u0000 y \uFFFF respectivamente.

Extensiones isNullOrEmpty y orEmpty

Las extensiones isNullOrEmpty y orEmpty sobre tipos tipo colección sobre nulables no son nuevas en la librería estándar. La primera devuelve true is el receptor es null o empty, y la segundadevuelve una instancia vacía si el receptor es null.

Esta versión mejora la consistencia de las APIs proporcionando una extensión orEmpty() para secuencias y una extensión isNullOrEmpty para colecciones, mapas y arrays de objetos.

Anunciando cambios breaking

Algunas correcciones de bugs introducen cambios que pueden llegar a ser breaking. Para asegurar una migración suave, deprecamos el comportamiento antiguo, informamos con advertencias y proporcionamos herramientas de migración siempre que sea posible, y siempre anunciamos dichos cambios por adelantado.

Nótese que esto es un mero anuncio, así que algunos de los cambios listados abajo, no están implementados aun y se implementarán en las próximas versiones hito.

Podéis consultar el changelog para averiguar qué cambios exáctamente están incluídos en esta versión.

Cambios en el compilador

  • KT-11567 Companion object instance field is no longer public when the companion object itself is not public
  • KT-16615 Initialization of properties without const modifier is changed so Java does not treat them like constants
  • KT-21354 An array is captured before the for-loop where it is iterated, so its reassignment inside the loop does not affect the iteration
  • KT-22275 The type of exceptions thrown from failed null checks is being unified to NullPointerException
  • KT-19532 Evaluation order of constructor arguments regarding <clinit> call is changed

Características deprecadas que ahora están prohibidas:

  • KT-16681 val reassignment in its getter
  • KT-16310 Nested classes in enum entries
  • KT-19618 Data classes overriding copy with the generated method
  • KT-17981 Inner classes inheriting Throwable that capture generic parameters from the outer class
  • KT-21515 Accessible classes and interfaces nested in companion objects
  • KT-25333 Java static members accessible through supertypes of companion object
  • KT-23153 Non-constant vararg annotation parameters
  • KT-23277 Local annotation classes
  • KT-22517 Smart casts for local delegated properties
  • KT-24197 mod operator convention. Note that operator rem should be used instead of modsince Kotlin 1.1
  • KT-20588, KT-20589 Passing single element to vararg in named form
  • KT-13762 Annotations with target EXPRESSION cannot have retention other than SOURCE
  • KT-22379 Smart cast is removed after while loops with break
  • KT-20830 Propagation of Java types nullability enhanced by annotations
  • KT-9580 setparam annotation target for a parameter declaration

Cambios en la librería estándar

  • KT-21784 The org.jetbrains.annotations package is being removed from the standard library in the compiler distribution. Note that the maven standard library artifacts are not affected.
  • KT-19489 Array.copyOfRange throws an exception when indices are out of bounds instead of enlarging the returned array
  • KT-17176 Progressions of ints and longs with a step of Int.MIN_VALUE and Long.MIN_VALUE are outlawed and won’t be allowed to be instantiated
  • KT-16097 Check for index overflow in operations on very long sequences
  • KT-21049 Unify split by an empty match regex result across the platforms

Nótas de versión preeliminar:

Nótese que las garantías que damos de compatibilidad hacia atrás no aplican a las versiones preeliminares: las características y APIs pueden variar en versiones posteriores. Cuando alcanzamos una RC final, todos los binarios producidos por versiones preeliminares dejarán de funcionar con el compilador, y os forzará a recompilar todo lo que tengáis compilado para las 1.3-Mx. Pero por supuesto, todo el codigo compilado con la 1.2.x y versiones anteriores funcionará con total normalidad sin necesidad de recompilar.

Cómo probar

En Maven/Gradle: Añadid http://dl.bintray.com/kotlin/kotlin-eap como repositorio para el buildscript y vuestros proyectos; y usad la versión 1.3-M1 para el plugin del compilador y la librería estándar.

En IntelliJ IDEA: Id a Tools → Kotlin → Configure Kotlin Plugin Updates, y seleccionad Early Access Preview 1.3 en el desplegable de Update channel y entonces haced click en Check for updates.

El compilador de línea de comandos se puede descargar desde la página de versiones de Github.

En try.kotlinlang.org: Usad el desplegable de abajo a la derecha para cambiar la versión del compilador de la 1.3-M1.