Saltar al contenido principal

De Wollok a Kotlin

Como se menciona en el titulo, esta guía busca dar un primer acercamiento hacia el lenguaje Kotlin, comparándolo con Wollok.

info

Según sus creadores, Wollok es un lenguaje de programación y entorno de desarrollo integrado (IDE) para ser utilizado en el dictado de materias iniciales de programación a objetos. El proyecto es dirigido por docentes universitarios de Argentina, nucleados en la Fundación Uqbar.

Migración en vivo#

A modo introductorio, un video donde migramos parte de un ejercicio hecho en Wollok a Kotlin, utilizando los entornos de desarrollo de cada herramienta.

Migrando Vendedores de Wollok a Kotlin

Grabación del proceso de migración de un ejercicio hecho en Wollok a Kotlin.

Definición de clases#

La primera gran diferencia es que Kotlin es un lenguaje con tipado estático mientras que Wollok tenía tipado dinámico. Esto implica que la responsabilidad de escribir los tipos pasa a ser de quien programa.

Afortunadamente Kotlin infiere la mayoría de los tipos, siendo necesario escribirlos en pocos casos:

  • parámetros, tanto de los métodos como del constructor de una clase;
  • atributos cuyo valor no se inicialize al definirlo.

Veamos un ejemplo de definiciones equivalentes en Wollok y Kotlin:

/* Wollok */class Ave {  var property energia  const property direccion
  const amiguis = []
  method esPopular() = amiguis.size() >= 5
  method conoceA(otroAve) = amiguis.contains(otroAve)
  method volar() {    energia -= 1  }
  method visitarA(otroAve) {    if (self.conoceA(otroAve)) {      self.volar()    }  }}
/* Kotlin */class Ave(var energia: Int, val direccion: String) {  val amiguis = mutableListOf<Ave>()
  fun esPopular() = amiguis.size >= 5
  fun conoceA(otroAve: Ave) = amiguis.contains(otroAve)
  fun volar() {    energia -= 1  }
  fun visitarA(otroAve: Ave) {    if (this.conoceA(otroAve)) {      this.volar()    }  }}

Algunas diferencias:

  • lo que en Wollok era const, en Kotlin es val. En ambos var significa lo mismo,
  • el method de Wollok es fun en Kotlin, y el self es this,
  • en Kotlin, los atributos se escriben como si fueran parámetros de la clase.

Instanciación de objetos#

Continuando con el ejemplo anterior, así se crean objetos en uno y otro lenguaje:

/* Wollok */const pepita = new Ave(energia = 5, direccion = "Wallaby 42, Sidney")
/* Kotlin */val pepita = Ave(energia = 5, direccion = "Wallaby 42, Sidney")
// También vale no poner los nombres de los atributos,// en ese caso lo que importa es el orden.val pepita = Ave(5, "Wallaby 42, Sidney")

Algunas diferencias:

  • en Kotlin, no se escribe el new;
  • en Kotlin, no es necesario poner los nombres de los atributos.

Objetos autodefinidos#

Para objetos únicos que tienen que ser conocidos por toda la aplicación, podemos crear objetos autodefinidos. Es decir, objetos que simplemente existen y no hace falta instanciarlos. En nuestro ejemplo, podemos usarlos para definir los tipos de alimentos.

object Alpiste {  var estaSeco = true  fun energiaQueOtorga() = if (estaSeco) { 4 } else { 2 }}
object Girasol {  fun energiaQueOtorga() = 4}
tip

Esto es casi igual que en Wollok, solo que acá escribimos los nombres de los objetos con MayusculaInicial.

Interfaces#

Como Kotlin utiliza tipado estático, si queremos tratar polimórficamente los dos objetos de arriba, necesitamos que tengan un tipo en común.

Podríamos crear una clase abstracta, pero en Kotlin también podemos crear una interfaz, que describe un contrato a cumplir:

interface Alimento {  fun energia(): Int}
object Alpiste: Alimento {  var estaSeco = true  fun energiaQueOtorga() = if (estaSeco) { 4 } else { 2 }}
object Girasol: Alimento {  fun energiaQueOtorga() = 4}
  • El : en este caso indica que nuestros objetos implementan la interfaz Alimento.
  • El override nos lo obliga a poner el IDE para aclarar que estamos hablando de una propiedad definida más arriba.

Luego, en Ave ya podemos definir:

fun alimentar(alimento: Alimento) {  energia += alimento.energiaQueOtorga()}

El método alimentar admite cualquier objeto que implemente la interfaz Alimento.

Colecciones#

Dos sabores: mutables o inmutables#

En Kotlin, todas las colecciones vienen en dos "sabores": mutables e inmutables. Las primeras soportan modificar sus elementos (agregar, quitar, actualizar), mientras que las segundas solo permiten acceder a sus elementos. Queda a criterio de quien programa cuál utilizar en cada caso, prefiriendo desde este espacio las inmutables (porque algo que no se puede modificar es menos propenso a errores).

Veamos entonces cómo crear unas y otras:

/* Kotlin */listOf(1, 2, 3)mutableListOf(1, 2, 3)
setOf(pepita, anastasia)mutableSetOf(pepita, anastasia)

Ojo 👀: no hay que mezclar las ideas de val y var con la (in)mutabilidad de las colecciones. Por ejemplo, una colección inmutable podría estar referenciada con var, mientras que una mutable podría ser val.

Closures o lambdas#

Hay una pequeña diferencia en cómo se escriben los closures que utilizamos para las operaciones de orden superior:

/* Wollok */avesAmigas.any { aveAmiga => aveAmiga.esPopular() }
/* Kotlin */avesAmigas.any { aveAmiga -> aveAmiga.esPopular() }

¿Te diste cuenta? En vez de usar => en Kotlin se usa -> para definir una lambda o closure.

Hay también otra sintaxis más "cheta" que tiene Kotlin, que nos ahorra de inventarle un nombre al parámetro:

avesAmigas.any { it.volarHastaLoDePepita() }

Cualquiera de las dos formas es válida.

Operaciones comunes sobre colecciones#

Un muy pequeño resumen de las operaciones más comunes. Una lista extensísima de todos los métodos que existen se pueden ver en la documentación oficial de Collection.

Básicas#

SintaxisComportamiento
sizeDevuelve el tamaño de la colección. Ojo que es un atributo, por eso va sin parentesis.
isEmpty()Indica si la colección está vacía.
first()Devuelve el primer elemento.
last()Devuelve el último elemento.
contains(elemento)Indica si la colección contiene a un cierto elemento.

Orden superior#

SintaxisComportamiento
any { condición }Indica si algún elemento cumple la condición dada.
all { condición }Indica si todos los elementos cumplen la condición dada.
find { condición }Devuelve el primer elemento que cumpla con la condición dada.
sumBy { transformación }Devuelve la suma de los elementos, aplicando la transformación sobre cada uno de ellos.
minByOrNull { transformación }Devuelve el menor elemento según la transformación dada, o null si la colección está vacía.

Están también los clásicos map, filter, reduce y muchísimos métodos más.

Exclusivos para colecciones mutables#

SintaxisComportamiento
clear()Borra todos los elementos.
add(elemento)Agrega el elemento.
remove(elemento)Borra el elemento.

Transformaciones entre lista y conjunto#

SintaxisComportamiento
lista.toSet()Devuelve un conjunto con los elementos únicos de la lista. O dicho de otra manera: los elementos que tenía la lista, omitiendo repetidos.
conjunto.toList()Devuelve una lista con los mismos elementos que tenga el conjunto.

Manejo de errores#

Para lanzar un error que corte la ejecución, en Wollok podemos hacer:

method volar() {  if (energia < 0) {    self.error("No queda suficiente energía para volar")  }
  energia -= 1}

En Kotlin, en vez de ser un método, hay una función global llamada error. El ejemplo se traduce a:

fun volar() {  if (energia < 0) {    error("No queda suficiente energía para volar")  }
  energia -= 1}

Otra forma de lanzar errores es usando check, que chequea que una condición se cumpla, y de lo contrario arroja un error con el mensaje especificado:

fun volar() {  check (energia >= 0) {    "No queda suficiente energía para volar"  }
  energia -= 1}

Queda al gusto de ustedes decidir cuál de las dos variantes utilizar. 😃