Guia rapida de Xtend

La siguiente es una guía de syntactic sugars de Xtend, algunos de los cuales trabajan conceptos más profundos que veremos a lo largo de la materia.

Definición de una clase

No tenemos objetos en Xtend, sólo clases. Aquí dejamos un ejemplo

      class Ave {
    static int ENERGIA_MINIMA = 10
    @Accessors int energia = 0
    def volar() { energia = energia - 10 }
    def comer(int cuanto) { energia = energia + (cuanto * 2) }
    def esFeliz() { energia > ENERGIA_MINIMA }
    def resetearEnergia() { energia = 0 }
}

    

Referencias variables y valores

En Xtend, al igual que muchos otros lenguajes, se diferencian las referencias como

      var String unString = "Pepito"
unString = "Otro String"

    
      val String constante = "Constante"
constante = "Otro"  // <----- NO COMPILA !

    

¡Ojo! no confundir el hecho de que no se pueda modificar la “referencia” de la mutabilidad/inmutabilidad del objeto al que apunta. Puedo tener un “val” apuntando a una colección, que es mutable.

      val List miLista = unaLista
miLista = otraLista  // <----- NO COMPILA: no puedo modificar la referencia
miLista.add(23)      // <---- SI COMPILA: puedo mandarle mensajes al objeto lista y agregarle elementos

    

Cuándo debería usar val y cuándo var

Tipos de datos

Strings

Un string se encierra entre dobles comillas, o bien podemos aprovechar para escribir un texto largo con enters con triples comillas simples, e insertar en el medio código Xtend mediante

      class Cliente {
    var nombre = "Juan" // definición con comillas dobles

    def presentacion() {
        '''
        Bienvenido, «nombre.trim()» a nuestra aplicación.
        En breve nos contactaremos con ud.
        '''
    }
}

    

Tip: para que te aparezcan los símbolos «» que son difíciles de encontrar en el teclado, simplemente utilizá las teclas Ctrl + Espacio dentro de la definición del string y aparecerán solas

Números

Existen muchos tipos de datos diferentes para números:

      var i = 0                         // int
var pi = 3.14d                    // double
var saldo = new BigDecimal(1500)  // BigDecimal
var Integer otroI = i             // otroI es un entero
otroI.bitwiseNot                  // puedo enviar un mensajes

    

Colecciones

Existen literales para definir listas, conjuntos y mapas (dictionaries):

      // Lista inmutable:
val myList = #['Hello', 'World']
// Set inmutable
val mySet = #{'Hello', 'World'}
// Mapa/Diccionario inmutable
val myMap = #{'a' -> 1 , 'b' ->2}

    

Recordemos que

Range

Es posible generar un rango de números, por ejemplo para iterar una cantidad de veces:

      #[1 .. 10].forEach [ ... ]      // [1..10] genera la lista de 1 a 10
#[1 ..< 10].forEach [ ... ]     // [1..<10] genera la lista de 1 a 9
#[1 >.. 10].forEach [ ... ]     // [1>..10] genera la lista de 2 a 10

    

..< es útil cuando necesitás iterar una lista de Java, que comienza en 0 y termina en (longitud - 1):

      #[0 ..< lista.size].forEach [ i | println(i) ]

    

Literales para lista, conjunto, etc.

Xtend trae shortcuts para definir diferentes tipos de colecciones:

      List<Factura> facturas = newArrayList
Set<Domicilio> domicilios = newHashSet
List<String> nombres = newArrayList("nahuel", "rodrigo", "marina")

    

Podés utilizar newLinkedList, emptyList, emptySet, emptyMap, newInmutableMap, newImmutableSet, newImmutableList, newLinkedHashSet, newTreeSet, newHashMap, newLinkedHashMap, newTreeMap. La ventaja que tienen es que permiten pasarle parámetros variables (tantos como elementos necesites) y trae implementaciones por defecto para algunas colecciones que necesitan comparators (tenés que estudiar más a fondo Colecciones en Xtend)

Inferencia de tipos

Xtend cuenta con inferencia de tipos, lo que permite

Vemos un ejemplo en vivo, mostrando cómo cambia la solapa “Outline” cuando modificamos el código:

image

Aquí vemos que incluso Xtend detecta expresiones que no tienen sentido, como cuando hicimos:

      def esFeliz() {
    energia > ENERGIA_MINIMA
    "si"
}

    

El hecho de generar una expresión energia > ENERGIA_MINIMA no causa efecto en el objeto y tampoco se devuelve (porque se pisa por la expresión “si” que es devuelta como retorno del método).

Volviendo a la inferencia de tipos, es fundamental poder contar con un lenguaje que tenga chequeo de tipos para detectar errores en forma temprana pero que no me obligue a definir los tipos todo el tiempo. La definición de tipos es obligatoria para las variables de instancia y de clase de los objetos, y en algunos casos cuando la definición de métodos polimórficos puede resultar ambigua para Xtend. En cualquiera de esos casos vas a ver un mensaje de error o de advertencia para que definas el tipo que mejor se ajuste.

Casteos

Si bien toda expresión tiene un tipo y Xtend suele inferirlo bastante bien, a veces es necesario hacer downcasting o forzar que una expresión pase por un tipo de datos:

      (42 as Integer)
(cliente as Cliente)

    

En general la expresión es, entre paréntesis: (valor/variable as tipo)

Definición de propiedades

La anotación @Accessors puede hacerse sobre una variable, como hemos visto antes:

      class Ave {
    @Accessors int energia = 0

    

En este caso se crean getters y setters para energia, transformándolo en una propiedad.

      class Ave {
    @Accessors(PUBLIC_GETTER) int energia = 0

    

En este caso se crea un getter público para la variable energia. Otras variantes son: crear sólo un setter público o crear getters o setters con diferentes visibilidad.

Por último, podemos anotar la clase con @Accessors

      @Accessors
class Ave {
    int energia = 0
    int vecesQueVolo = 0

    

en este caso, se crean getters y setters para todas las variables de dicha clase.

Shortcut para acceder a propiedades

Cuando usamos un objeto que tiene propiedades (par getter y setter), podemos cambiar un poco la sintaxis para que se vea más simple. En el ejemplo anterior del Ave:

      val pepita = new Ave()
pepita.energia = 100    // <-- equivale a pepita.setEnergia(100)
pepita.energia          // <-- equivale a pepita.getEnergia()

    

¡Ojo! si bien parece que estamos accediendo diréctamente a la variable de instancia, no es así. Xtend simplemente traduce esa sintaxis a la anterior. Es decir que en ambos casos estamos igualmente llamando al getter y al setter. Pueden probar eliminando la anotación @Accessors y recibiremos un mensaje “The field energia is not visible”.

Paréntesis en el envío de mensajes

No es necesario utilizar paréntesis, ni en la creación de objetos, ni en el envío de mensajes sin parámetros:

      val golondrina = new Ave
golondrina.resetearEnergia

    

Herencia y redefinición de métodos

A continuación vemos cómo definir dos subclases de Ave: Golondrina y Torcaza.

image

      @Accessors
class Ave {
    int energia = 0
    static int ENERGIA_MINIMA = 10
    def volar() { energia = energia - 10 }
    def comer(int cuanto) { energia = energia + (cuanto * 2) }
    def esFeliz() { energia > ENERGIA_MINIMA }
}

class Golondrina extends Ave {
    override esFeliz() { true }
}

class Torcaza extends Ave {
    int vecesQueVolo = 0

    override volar() {
        super.volar()
        vecesQueVolo++
    }
}

    

Aquí vemos que

Clases y métodos abstractos

Podemos definir a Ave como clase abstracta, esto producirá que no podamos instanciar objetos Ave. Una clase abstracta puede definir solo la interfaz de un método, lo que se conoce como método abstracto. Veamos el siguiente ejemplo:

image

En el ejemplo:

y finalmente todo compila.

Constructores

Un constructor se define con la palabra reservada new (equivalente al constructor de Wollok):

      @Accessors
class Golondrina {
    int energia
    new() {
        this(100)   // llama al constructor con parámetros
        // para llamar al constructor de la superclase es necesario utilizar super(params)
    }
    new(int energia) {
        this.energia = energia
    }
}

    

Bloques

Un bloque permite definir una porción de código, también llamada expresión lambda:

      val cuadrado = [ int num | num ** 2 ]
cuadrado.apply(5)

    

De esta manera podemos enviar bloques como parámetros, algo muy útil para trabajar entre otras cosas con las colecciones (map, filter, fold, etc.)

La sintaxis general es

      [ | ... ]                // bloque sin parámetros
[ elem | ... ]           // bloque con un parámetro
[ int a, int b | a + b ] // bloque con dos parámetros

    

Variable implícita it

De la misma manera que cuando estamos dentro de una clase, podemos acceder a una variable de instancia con this

      this.energia

    

o sin él:

      energia

    

también podemos usar una variable implícita it dentro de un método.

      val it = new Ave()
volar       // equivale a it.volar()
comer(2)    // equivale a it.comer(2)

    

Dentro de una expresión lambda, it es la variable implícita del primer parámetro, por lo tanto todas estas expresiones son equivalentes:

      alumnos.filter [ alumno | alumno.estudioso() ]
alumnos.filter [ it | it.estudioso() ]
alumnos.filter [ it.estudioso() ]
alumnos.filter [ it.estudioso ]
alumnos.filter [ estudioso ]

    

image

Manejo de nulls

Los valores nulos son siempre un dolor de cabeza, Xtend tiene algunos trucos para facilitar un poco más el trabajo con ellos.

Elvis operator

Parece un emoticón, pero ?: es un shortcut para utilizar un valor por defecto cuando una expresión pueda ser nula:

      val nombre = person.firstName ?: 'You'

    

Si la expresión que está a la izquierda se evalúa como null, nombre se asigna a la segunda expresión.

Null safe operator

También podemos resolver envíos de mensajes a referencias que potencialmente podrían ser nulas:

      val mejorAlumno = alumnos.find [ ... ]
...
mejorAlumno?.felicitar()

    

En este caso, el operador ?. es equivalente a preguntar if (mejorAlumno) mejorAlumno.felicitar()

Comparar referencias

Después de varios cambios, Xtend dejó las cosas como la mayoría de los lenguajes. Tenemos dos formas de comparar referencias:

      ref1 == ref2     // compara por igualdad, esto significa que son iguales si son las referencias
                 // apuntan al mismo objeto o bien, en base a la definición del método equals
                 // en la clase ref1 (sabiendo que ref1 no es nulo)
                 // en la clase asociada a ref1
ref1 === ref2    // compara por identidad, esto significa que son iguales si las referencias
                 // apuntan al mismo objeto en memoria, determinado por la VM y no se puede cambiar

    

Tener especial atención a los strings, ya que dos strings con el mismo contenido pueden ser iguales pero no idénticos, dependiendo de las estrategias de optimización de la VM. Siempre es conveniente utilizar ==, que además se puede modificar.

Métodos avanzados

Obligatoriedad del return en métodos

Por lo general, los métodos devuelven la última expresión que contienen. Pero a veces es necesario cortar el flujo de envío de mensajes, como por ejemplo aquí:

      def gradoDeFelicidad() {
    if (!esFeliz) {
       return 0
    }
    ... cálculo complejo ...
}

    

Para determinar el grado de felicidad de alguien, tenemos como precondición que sea feliz. Y para simplificar la definición, escribimos el if y forzamos el return (dado que escribir únicamente 0 no tendrá efecto, porque Xtend seguirá evaluando el resto de las expresiones hasta terminar la última y nosotros queremos justamente cortar el flujo).

En el caso de un método que solo busque producir un efecto (void), es necesario utilizar return; con punto y coma…

      def metodoConEfecto() {
    ... cambios ...
    if (!situacion) {
       return;
    }
    ... otros cambios ...
}

    

Igualmente, siempre es preferible tratar de extraer métodos más pequeños para simplificar la lógica.

Extension methods

Una de las herramientas más poderosas consiste en definir extension methods. Supongamos que un negocio tiene un horario de apertura y de cierre y queremos saber, dada una hora, si está abierto.

      class Negocio {
    int horarioApertura
    int horarioCierre

    def estaAbierto(int horaActual) {
        horaActual.between(horarioApertura, horarioCierre)
    }
}

    

Por supuesto, no compila. No existe el método between asociado a los enteros. Pero en otro archivo vamos a definir un método estático (no asociado a un objeto):

      class NumberUtils {
    static def between(int num, int from, int to) {
        num >= from && num <= to
    }
}

    

Y ahora, desde el archivo Negocio.xtend, vamos a importar el método como static extension:

image

El código a agregar es

      // después de la definición de package
import static extension ar.edu.unsam.prueba.NumberUtils.*

    

Esto produce que automáticamente, el compilador Xtend marque en naranja el método between y nuestra clase Negocio compile perfectamente. En resumen, un extension method permite que nosotros agreguemos comportamiento por afuera de la definición de una clase como si estuviéramos trabajando en ella, algo muy importante cuando la clase no podemos modificarla (como en el caso de Integer , String), o bien cuando se está regenerando todo el tiempo (cuando tenemos un framework que genera código para nosotros), sin contar que además estamos respetando la idea de mensaje (y por consiguiente, la posibilidad de seguir trabajando con polimorfismo).

Los métodos map, filter, fold, length, any, etc. son todos extension methods de Collections.

Dispatch methods

Xtend permite trabajar con multimethods, más adelante tendremos este ejercicio para contarlo con más profundidad

@Data

Para definir un objeto inmutable, debemos:

Xtend provee la anotación @Data para lograr eso:

      @Data
class Point {
    int x
    int y
}

    

Esto equivale a definir el constructor con dos parámetros (x, y) y los getters para los atributos x e y, sin setters (dado que queremos únicamente representar un valor). Por lo tanto, esta definición compila perfectamente:

      class TestPoint {
    def test() {
        new Point(2, 4).x
    }
}

    

Si por el contrario intentamos asignar el valor de x:

      new Point(2, 4).x = 2

    

nos dirá The field x is not visible.

With operator

Otro syntactic sugar muy interesante de Xtend es la posibilidad de enviar múltiples mensajes al mismo objeto, mediante el operador with =>, algo muy útil cuando estamos instanciando objetos:

      val ventaNacional = new Venta => [
    cantidadKilos = 12
    fechaVenta = new Date
    parcela = parcela50
    comprador = new CompradorNacional
]

    

De esta manera, todos los mensajes se apuntan al objeto que resulta de evaluar la expresión new Venta, y simplifica el envío de mensajes:

      ventaNacional.cantidadKilos = 12
ventaNacional.fechaVenta = new Date
ventaNacional....