Iterables, secuencias y tuplas

Esta es la sexta parada de el tour de Ceylon. En la parada anterior cubrimos las clases anónimas y las clases miembro. Ahora veremos acerca de los objetos iterables, las secuencias y las tuplas. Estos son ejemplos de objetos contenedores genéricos. No te preocupes hablaremos mas de los objetos genéricos mas adelante.

Iterables

Un objeto iterable es un objeto que produce un flujo de valores. Los objetos iterables satisfacen la interfaz Iterable.

Ceylon provee algo de azúcar sintáctica para trabajar con objetos iterables.

  • El tipo Iterable<X,Null> representa un objeto iterables que tal vez no genere algún valor cuando es iterado, y puede ser abreviado {X*}, y
  • El tipo Iterable<X,Nothing> representa un objeto iterables que siempre produce al menos un valor cuando es iterado, y es usualmente abreviado {X+}.

También podemos construir una instancia de Iterable usando llaves:

{String+} palabras = { "hola", "mundo" };
{String+} masPalabras = { "hello", "world", *palabras };

El prefijo * es llamado spread operator. El “extiende” los valores de un objeto iterable. Así masPalabras produce los valores “hello”, “world”, “hola”, “mundo” cuando es iterado.

Como veremos después, las llaves pueden incluso contener comprenhension, haciéndolo mucho mas poderoso que lo que hemos visto.

Iterable es un subtipo de la interfaz Category así es que podemos usar el operador in para probar si el valor es producido por un Iterable.

if (exists char = text[i],
    char in {',', '.', '!', '?', ';', ':'}) {
    //...
}
"index debera estar entre 1 y 100"
 assert (index in 1..100);

El operador in`es solo azúcar sintáctica para el método `contains de Category`.

Iterando con for

Para iterar una instancia de Iterable, podemos usar un ciclo for:

for (palabra in masPalabras) {
     print(palabra);
}

Si, por alguna razón, necesitamos un indice para cada elemento producido por por un objeto iterable, podemos usar una variación espacial de el ciclo for que ha sido diseñado para iterar Entry:

for (i -> palabra in entries(masPalabras)) {
    print("``i``: ``palabra``");
}

La función entry devuelve una instancia de Entry<Integer,String>`[] conteniendo los elementos indexados de la secuencias. (La `-> es azúcar sintáctica para la clase Entry.)

Esto frecuentemente útil para iterar dos secuencias a la vez. La función zip() es practico en el siguiente caso:

for (nombre -> lugar in zip(nombre,lugar)) {
     print(nombre + " @ " + "lugar");
}

Secuencias

Algunos tipos de array o listas es una característica universal de todos los lenguajes de programación. El modulo del lenguage de Ceylon define soporte para tipo secuencia a través de las interfaces Sequential, Sequence y Empty.

De nuevo, hay mas azúcar sintáctica asociada con secuencias:

  • El tipo Sequential<X> representa una secuencia que puede puede ser vacía y puede ser abreviada [X*] o X[],
  • El tipo Sequence<X> representa una secuencia no vacia y puede ser abreviada [X+],
  • El tipo Empty representa una secuencia vacía y es abreviada [].

Algunos operaciones de el tipo Sequence no son definida por Sequential, asi que no llamaras estas si lo que tienes es X[]. Sin embargo, necesitamos la construcción if (nonempty ...) para tener acceso a estas operaciones.

void printBound(String[] strings) {
    if (nonempty strings) {
        //strings es de tipo [String+] en este bloque
        print(strings.first + ".." + strings.last);
    }
    else {
        print("Empty");
    }
}

Note como este es solo la continuación de el patrón establecido para el manejo de null. De hecho, ambas construcciones son solo abreviaciones para la reducción de tipos:

  • if (nonempty strings) es una abrecian para if (is [String+] strings), al igual que
  • if (exists name) es una abreviación para para if (is Object name).

Azúcar sintáctica en secuencias

Hay mucho mas azúcar sintáctica para secuencias. Podemos utilizar un grupo de sintaxis de Java:

String[] operators = [ "+", "-", "*", "/" ];
String? plus = operators[0];
String[] multiplicative = operators[2..3];

Oh, y la expresion [] evalua a una instancia de Empty.

Sin embargo, a diferencia de Java, todas estas construcciones son solo abreviaciones. El código anterior es exactamente equivalente a el siguiente código sin azucarado:

Sequential<String> operators = ...;
Null|String plus = operators.get(0);
Sequential<String> multiplicative = operators.span(2,3);

(Volveremos en unos minuto para ver que significa una lista de valores dentro de corchetes.)

La interfaz Sequence extiende a Iterable, así que podemos iterar Sequence usando un ciclo for:

for (op in operators) {
    print(op);
}

Rangos

Un range es un tipo de Sequence. El siguiente codigo:

Character[] uppercaseLetters = 'A'..'Z';
Intergers[] countDown = 10..0;

Es solo azúcar para:

Sequential<Character> uppercaseLetters = Range('A','Z');
Sequential<Integer> countDown = Range(10,0);

De hecho, esto es solo una pequeña vista de el hecho que case todos los operadores son solo azúcar para llamar a métodos a un tipo. Volveremos a esto mas adelante, cuando hablemos acerca de polimorfismo en operadores.

Ceylon no necesita un for al estilo de C. En vez de ello, combina un for con un operador de rango.

variable Integer fac=1;
for (n in 1..100) {
    fac*=n;
    print("Factorial ``n``! = ``fac``");
}

Secuencias y sus supertipos

Es probablemente un buen momento para ver código mas avanzado en Ceylon. Que mejor lugar para encontrar que en modulo del lenguaje mismo.

Puedes encontrar le documentación de la API y su código de sequence en linea, o puedes ir directamente a Navigate > Open Ceylon Declaration...” para ver la declaración de `Sequential directamente en la IDE de Ceylon.

Las operaciones mas importantes de Sequential son heredadas de Correspondence e Iterable.

  • Correspondence provee la capacidad de acceder a elementos por medio de indices, y
  • Iterables provee la habilidad de iterar los elementos de una secuencia.

Ahora abre la clase Range en el IDE, para ver una implementación concreta de la interfaz Sequence.

Secuencias vacías y el tipo de fondo

Finalmente, revisemos la definición de Empty, Note que Empty es declarado como un subtipo de List<Nothing>. Este tipo especial Nothing, es llamado el tipo de fondo, representa:

  • El conjunto vació, o equivalentemente
  • La intersección de todos los tipos.

Desde que el conjunto vació es un subconjunto de todos los otros conjuntos, Nothing es asignable a todos los otros tipos. ¿Por que esto es útil aquí? Bueno, Correspondence<Integer,Element> e Iterable<Element> son ambos covariantes en el parámetro de tipo Element. Así Empty es asignable a Correspondence<Integer,T> y Iterable<T> para cualquier tipo T. Esto es el por que no se necesita un tipo de parámetro.

Desde que no hay instancias actuales de Nothing,si alguna vez ves un atributo o método de tipo Nothing, tendrás la certeza que no es posible que revuelva un valor. Esto es un camino posible para que tal operación termine con una excepción.

Otra cosa a notar es el valor de retorno de first e item() de Empty. Tal vez hallas estado esperando ver Nothing? aquí, desde que ellos sobreescriben los miembros del supertipo de tipo T?. Pero como hemos visto en la primera parte del tour, Nothing? es solo una abreviación para Null|Nothing. Y Nothing es el conjunto vació, así la unión Nothing|T de Nothing con algún otro tipo T es solo T.

El compilador de Ceylon esta habilitado para hacer todo este razonamiento automáticamente. Así cuando el vea un Iterable<Nothing>, el sabe que la operación first es de tipo Null, por ejemplo, que su valor es null.

Cool, ¿no?

Gotchas para Desarrolladores de Java

Superficialmente, un tipo secuencia luce cono un array de Java, ¡pero realmente es muy muy diferente! Primero, por supuesto, un tipo secuencia Sequential<String> es una interfaz inmutable, este no es un tipo concreto mutable como un array. No podemos establecer un valor de un elemento:

String[] operators = ....;
operators[0] = "`"; //compile error

Ademas, la operación indice operators[i] devuelve un tipo opcional String?, que resulta en código un poco diferente al idioma. Para comenzar, no iteramos secuencias por indice como en C o Java. El siguiente código no compila:

for (i in 0..operators.size-1){
    String op = operators[i]; // compile error
}

Aquí, operators[i] es de tipo `String? que no es directamente asignable a String.

En vez de ellos, si necesitamos acceder a el indice, usaremos la forma especial de for mostrada anteriormente.

for (i -> op in entries(operators)) {
    //...
}

Así mismo, frecuentemente no hacemos una prueba adelantada de un indice contra la longitud de la secuencia.

if (i>operators.size-1) {
    throw IndexOutBoundException();
}
else {
    return operators[i]; //compile error
}

En su lugar, hacemos la prueba después de acceder al elemento de la secuencia:

if (exists op = operators[i]) {
    return op;
}
else {
    throw IndexOutBoundException();
}

De hecho este es un uso común para assert:

assert(exists op = operators[i]);
return op;

Especialmente nunca necesitaremos escribir lo siguiente:

if (i>operators.size-1) {
    return "";
}
else  {
    return operators[i]; //compile error
}

Es mucho mas limpio y elegante:

return operators[i] else "";

Todo esto puede que tome algo de tiempo empezar a usarlo. Pero es agradable que este mismo idioma aplique a otros tipos de Correspondence. incuyendo Map.

Tuplas

Tuplas es una lista ligada que captura el tipo estático para cada elemento individual en la lista. Por ejemplo:

[Float, Float, String] point = [0.0, 0.0, "origin"];

Esta tupla contiene a dos Float seguidos por una String. Esta información es capturada en su tipo estático, [Float, Float, String].

Cada liga de la lista es una instancia de la clase Tuple. Si realmente deseas conocer, el código anterior es solo azúcar sintáctica para el siguiente código:

Tuple<Float|String,Float,Tuple<Float|String,Float,Tuple<String,String>>>
    punto = Tuple(0.0, Tuple(0.0, Tuple("origin", [])));

Sin embargo, siempre usaremos la azúcar sintáctica cuando trabajemos con tuplas.

Tuple extiende Sequence, así que podemos hacer todas las cosas usuales con secuencias con las tuplas. Como con una secuencia, podemos acceder a un elemento de la tupla por indice. Pero en el caso de una tupla, Ceylon esta habilitado para determinar el tipo de el elemento cuando es indexado por un entero literal.

Float x = point[0];
Float y = point[1];
Float label = point[2];
Null zippo = point[3];

Una tupla no terminada(unterminated) es cuando la ultima liga en la lista es una secuencia, no un Empty. Por ejemplo:

String[] labels = ...;
[Float, Float, String*] point = [0.0, 0.0, \*labels];

Esta tupla contiene dos Float seguido por un numero no conocido de String.

Ahora podemos ver que un tipo secuencia como [String*] o [String+] puede ser vista como un tipo de tupla degenerada.

Aun hay mas

Si estas interesado, puedes encontrar una discusión mas en profundidad de tuplas aqui.

En adelante vamos a explorar algunos detalles mas de el sistema de tipos, comenzando con tipos alias e inferencia de tipos.