style="display:inline-block;width:728px;height:90px"
data-ad-client="ca-pub-5164839828746352"
data-ad-slot="7563230308">

Tipos en Ceylon: Un sistema de tipos con sentido

Para mi primer post acerca de Ceylon, quiero hablar un poco acerca del sistema de tipos. Una de las metas de Ceylon es la legibilidad del código; otra es tener un sistema de tipos que sea sencillo pero a la vez poderoso (Ceylon es un lenguaje de tipado estático). A continuación pretendo demostrar cómo estos dos objetivos se unen, dando como resultado código más legible, con un ejemplo sencillo: una lista heterogénea (es decir, que contiene elementos de distintos tipos). Veamos el ejemplo en un par de lenguajes de tipado estático, incluso en algunos de tipado dinámico. Primero obviamente en Java:

List<Object> list = Arrays.asList(new Object[]{ 0, "uno", 2, "tres", 4, "cinco" });
for (Object elem : list) {
  if (elem instanceof String) {
    //Para hacer algo con el string requerimos un cast
    String minusc = ((String)elem).toLowerCase();
    System.out.println(minusc);
  } else if (elem instanceof Number) {
    //Igual aqui
    int doble = 2 * ((Number)elem).intValue();
    System.out.println(doble);
  }
}

Las colecciones en Java con generics nos permiten definir colecciones de un solo tipo; si queremos almacenar tipos distintos, como en este caso enteros y cadenas, hay que definir algún supertipo en común, y pues en este caso únicamente puede ser java.lang.Object.

Por lo tanto, cuando recorremos la lista, el elemento sobre el cual iteramos debe ser un Object. Si queremos hacer algo con él, primero hay que revisar de qué tipo es. Y aquí es donde ya se empieza a sentir incómodo el asunto: cuando utilizo instanceof, pues ya estoy seguro de que el elemento es de ese tipo, no? Entonces, ¿por qué debo hacer un cast en un bloque de código que todos sabemos que únicamente se ejecutará cuando el elemento es un String? Como que eso sobra, ¿no? Si el compilador fuera tantito inteligente, podría hacer ese cast por nosotros.

Scala también es un lenguaje de tipado estático, y tiene una manera distinta de tratar con objetos de distintos tipos. Veamos:

//Con inferencia de tipos, no necesitamos declarar el tipo de list
//Pero sabemos que será una List[Any] porque es el único tipo en común.
val list = 0::"uno"::2::"tres"::4::"cinco"::Nil
list.foreach { elem => //por lo tanto, elem es tipo Any
  elem match { //pero gracias a pattern matching...
    case s:String => //podemos tener casos según el tipo de elem
      println(s.toLowerCase())
    case i:Int =>
      println(2*i);
  }
}

Entonces, en Scala podemos ahorrarnos el cast; mucho mejor que en Java. La única bronca es que si esa lista viene de otro lado (nos la entrega un componente de una biblioteca externa por ejemplo), no hay manera de saber si sólo trae enteros y cadenas; podría traer cualquier otra cosa. Sólo leyendo la documentación podríamos saber. Lo mismo va para el código en Java.

Si en Java ejecutamos esa iteración con una lista que tuviera un URL, por ejemplo, simplemente se salta ese elemento porque no cae en ninguno de los dos casos. En Scala, al llegar al URL se arroja una excepción tipo scala.MatchError porque no está contemplado ese caso. No sé qué es peor. Obviamente la solución en Java es poner un else para manejar todos los demás casos y en Scala hay que poner un case _ => para manejar el caso de cualquier otro tipo.

En un lenguaje dinámico como Groovy, podemos determinar el tipo y no necesitamos hacer un cast, porque la invocación al método se resolverá de manera dinámica, en tiempo de ejecución:

def list = [0, 'uno', 2, 'tres', 4, 'cinco']
list.each { //tenemos el parámetro implícito "it"
  if (it instanceof String) {
    println(it.toLowerCase())
  } else if (it instanceof Number) {
    println(it*2)
  }
}

Incluso, podemos hacer uso del switch de Groovy, que puede incluir el tipo de objeto:

def list = [0, 'uno', 2, 'tres', 4, 'cinco']
list.each { //tenemos el parámetro implícito "it"
  switch (it) {
    case String:println it.toUpperCase()
      break
    case Number:println(it*2)
      break
  }
}

Es muy importante no olvidarse del break en cada caso. Si no hubiera break entre el caso de String y el de Number, por ejemplo, entonces cada cadena se imprimiría en mayúsculas y después dos veces, porque en Groovy el operador de multiplicación aplicado a una cadena la repite tantas veces como se indique (es decir, "X"*2 devuelve XX). Para manejar el caso de elementos que no sean números ni cadenas, hay que agregar al final del switch el caso default.

Bueno, pero ¿y en Ceylon?

Antes de las explicaciones, veamos el código en Ceylon:

value list = { 0, "uno", 2, "tres", 4, "cinco" };
for (elem in list) {
  if (is String elem) {
    print(elem.uppercased);
  } else if (is Integer elem) {
    print(elem*2);
  }
}

Primero que nada, tenemos la declaración de una secuencia, asignada a un valor inmutable. Solamente requerimos value sin indicar el tipo porque tenemos inferencia de tipos en declaraciones locales. Para declarar secuencias podemos usar llaves; las secuencias son inmutables por default. Pero lo interesante es que en Ceylon tenemos unión e intersección de tipos; list es de tipo Sequence<Integer|String>. Esto es muy conveniente porque ahora ya sabemos que la secuencia puede contener solamente cadenas y enteros, aunque sean tipos dispares que no tengan nada en común más allá de la clase raíz. Sabiendo esto, no es necesario manejar el caso de que la secuencia traiga elementos de algún otro tipo, porque la definición indica que sólo son cadenas y enteros.

La unión de tipos se hace con el pipe: String|Integer, y significa que el valor puede tener cualquiera de los dos tipos; la intersección se hace con el ampersand: Runnable&Serializable, y significa que el valor debe extender ambos tipos.

La iteración sobre la secuencia es con el for, similar al de Java, pero aquí también tenemos inferencia de tipos porque es una declaración local nuevamente. Si tuviéramos que declarar el tipo de elem, es simplemente String|Integer.

Dentro del ciclo, hacemos uso del operador is, que devuelve true cuando el valor indicado es del tipo solicitado (o algún subtipo del mismo). Y si la condición se cumple, el compilador ya trata a elem como un String dentro de ese bloque de código (y como Integer en el otro bloque alterno). No necesitamos un else porque ya quedó establecido que la secuencia sobre la que estamos iterando solamente puede contener enteros y cadenas.

En el release inicial M1 de Ceylon, aún no hay soporte para la sentencia switch, pero la manera en que va a funcionar es una especie de mezcla entre el match de Scala y el switch de Groovy:

value list = { 0, "uno", 2, "tres", 4, "cinco" };
for (elem in list) {
  switch (elem)
  case (is String) {
    print(elem.uppercased);
  }
  case (is Integer) {
    print(elem*2);
  }
}

El switch en Ceylon tiene otras peculiaridades, pero en este caso, lo importante es que se pueden tener casos con el operador is, para cada caso debe haber un bloque, no se necesita break, y dentro del bloque de una condición is, el valor evaluado se trata como del tipo verificado, lo cual hace innecesario un cast.

Por último, aunque no tiene mucho que ver con el sistema de tipos, aprovecho para mencionar una característica adicional del for en Ceylon: tiene else. Si el ciclo termina de manera normal (se llega al último elemento), se ejecuta el else, si está definido. Esto es muy útil para iteraciones sobre secuencias cuando se está buscando algún elemento o alguna condición específica; si se encuentra, se termina el ciclo con un simple break, pero si se recorre toda la secuencia y no se cumplió la condición, se ejecuta el else. Supongamos que queremos terminar el ciclo al encontrar un número impar en la secuencia:

value list = { 0, "uno", 2, "tres", 4, "cinco" };
for (elem in list) {
  if (is Integer elem) {
    if (elem % 2 == 1) {
      print("Encontramos el número impar!");
      break;
    }
  }
} else {
  print("Solamente hay números pares en la secuencia");
}

Dado que los números de la lista son todos pares, se ejecutará el else.

Pues bien, espero que en un futuro no muy lejano esto sirva como referencia para los que quieran adentrarse a la programación con Ceylon, y para picar la curiosidad a partir de hoy mismo de quienes tengan la inquietud de conocer nuevos lenguajes de programación.

Comentarios

Opciones de visualización de comentarios

Seleccione la forma que prefiera para mostrar los comentarios y haga clic en «Guardar las opciones» para activar los cambios.

Supongo que vale la pena

Supongo que vale la pena mencionar que el uso de los tipos union para la inferencia de argumentos genericos funciona bien porque Ceylon tiene covariancia para los tipos genericos. Cuando vemos una sequencia como:

value list = { 0, "uno", 2, "tres", 4, "cinco" };

O instanciacion de una clase generica, como:

value map = HashMap { 1->"uno", 2.0->"dos", 3->"tres", 4.0->"cinco" };

El tipo princpal invuelcra un tipo union.:

Luego, observamos que Sequence y Map son tipos covariantes, entonces esto tambien es bien tipado:

Sequence<Object> list = { 0, "uno", 2, "tres", 4, "cinco" };
Map<Number,String>  map = HashMap { 1->"uno", 2.0->"dos", 3->"tres", 4.0->"cinco" };

Y esto de hecho es lo que queremos decir cuando utilizamos la frase "tipo principal" que es tan importante en el diseño de Ceylon: estamos diciendo que el tipo principal como Sequence<Integer|String> es un subtipo de cualquier otro tipo que podamos querer. Y el chiste es que el compilador puede determinar el tipo principal de { 0, "uno", 2, "tres", 4, "cinco" } o HashMap { 1->"uno", 2.0->"dos", 3->"tres", 4.0->"cinco" } sin que escribamos los argumentos Integer|String o Integer|Float. Compara:

//Java 7
List<String> list = createList<>("foo", "bar");
//Ceylon
value list = createList("foo", "bar");

Esto si es bien chingonsisimo.

Imagen de Abaddon

Interesante

Esta interesante el esquema de definición de valores usando Unión e Intersección.

Muy buena explicación Enrique.

Aguas con el Switch

Leyendo la sintaxs del switch puedo entender que siempre vas a evaluar booleanso. Si bien es cierto que el operador is siempre retorna booleano. Me parece que una manera mas clara es:

switch (elem)
  case (String) {
    print(elem.uppercased);
  }
  case Integer {  // sin las cucarachitas estas ( )
    print(elem*2);
  }

En este caso, si entenderia que se esta evaluando que sea String o Integer. Actualmente en java (y no es que quiero que se parezca a Java o a X lenguaje) se hace

case 1:

y no

case  == 1:

finalmente seria estetica pensarian ustedes pero no. Mi punto de vista 1 es uno tal cual mientras que == 1 es true|false y lo mismo pasa con el operador este de ceylon que regresa booleanos

En cuanto a lo del for esta genial, no se que tan acertado sea mi opinion pero yo lo veo que es el catch del for aunque seria de la roptura del ciclo controlada porque si entras en un ArrayIndexOutOfBounsException es claro que no entraria al else. Recuerdo que Gaving comentaba algo que decia mas o menos asi: "En ceylon no hay Excepciones al menos que vengan de un codigo Java" bueno en ese caso si vendria como contradiccion a lo que digo de llamarle "catch del for" a ese else

Imagen de ezamudio

no es catch

for/else no es un catch, es un else. No veo cómo pueda arrojarse IndexOutOfBoundsException en un foreach; no tienes acceso al iterador. En Ceylon no hay NullPointerExceptions a menos que vengan de código Java.

El switch tendrá muchas más cosas; si ves toda la spec del switch ya queda claro por qué se pone is y no solamente una clase. De hecho si haces switch sobre un tipo, puedes poner casos con satisfies; puedes poner más condiciones aparte del puro is, yo puse un ejemplo muy simple pero créeme que va a ser algo bastante poderoso. Según entiendo, podrás hacer algo así:

switch (x) {
  case (is Integer) {
    switch (x) {
      case (1,3,5) { /* bla */ }
      case (2, 4, 6) { /* bla */ }
      case (7) { /* bla */ }
      else { /* bla */ }
    }
  case (is String) { /* bla */ }
}

Variantes más sofisticadas, haciendo el switch sobre un tipo y no sobre un valor:

void mifuncion<T>(T parm)
    given T of String|Integer {
  switch (T) {
    case (satisfies String) { /* bla, aquí parm es tratado como String */ }
    case (satisfies Integer) { /* aquí parm es tratado como Integer */ }
  }
}

Como nota al margen: El switch de Java está mucho más limitado. Apenas en la versión 7 ya se puede hacer switch sobre cadenas...

En Ceylon, String es una

En Ceylon, String es una expresion que tiene el tipo Class<String>. Puedo escribir cosas como:

Class<Foo> fooClass = Foo;
Foo foo = fooClass();

Esto es el metamodelo "tipado estatico" de Ceylon. Bueno, aun nos falta implementarlo pero ya existe el la spec.

Entonces tiene ambigüedad si el case tambien tendrá otra forma para comparar valores en lugar de tipos. Considera:

case (x)
case (String) { ... }
case (Integer) { ... }
else { ... }

Se refiere al tipo de x o a su valor?

(Aun no hemos decidido si realmente necesitamos esta otra forma. Yo personalmente casi no uso el case de Java excepto con los tipos enumerados.)

P.S. Se necesitan las cucarachitas por que Foo{} es unu expresion legal en Ceylon. No se puede eliminar las parentesis de `if`, `for`, etc.

De hecho el ultimo ejemplo

De hecho el ultimo ejemplo aqui no captura completamente lo que quiero demostrar. Mejor:

//Java 7
List<Number> list = createList<>(1, 1.0);
//Ceylon
value list = createList(1, 1.0);

En este caso quizás podría

En este caso quizás podría ser igualmente inteligente y evaluar tipo o valor. Por ejemplo

    case (x)
    case (String) { ... }
    case (123) { ... }

¿Que quiero decir? A pues en la primera linea evaluamos x, en la segunda comparamos con String. ah pero que cosa es String, ¿un valor?, que valor tiene... ah no, es un tipo entonces podría hacer la comparación por tipo. Después, en la linea 3 comparamos con 123. Pero, que cosa es 123 ¿un valor? Si, ¿tiene tipo? CLARO! 123 es de tipo Integer. Entonces digamos que se está evaluando el valor de tipo Integer no?. Es así como a mi punto de vista podría omitirse el is

Se me ocurre que intente evaluar un numero ya sea de tipo Integer o Long. Tomando las bondades de Ceylon quizás se pueda utilizar:

    case (is Integer, is Long) { ... } // entiendo sue así quedaría hoy en día
    case (Integer|Long) { ... } // usando las bondades de Ceylon

Quizás lo mas seguro es que pongamos: case (is Number) pero quizás para los decimales necesitemos hacer otra cosa.


Editado:
P.S. Lo de las cucarachitas, eso si es estetico y no creo que cause ruido, solo lo comentaba porque como dices en if, for se usan, pero me brincó que en el case si. Nada especial
Imagen de ezamudio

Builder pattern

Cierto, en el switch no se puede poner nada más case MiClase { blabla } porque MiClase {} es la sintaxis para construir un objeto de MiClase...

java.daba.doo, en todo caso como dices "para los decimales", podrías hacer algo así:

case (is Integer|Long) { blabla }
case (is Float|BigDecimal) { blabla }

Como nota al margen, Integer es un número entero de 64 bits en Ceylon (el equivalente de Long en Java). Así que pues no hay Long (o más bien, no hay int32).

Esto es el punto: si es un

Esto es el punto: si es un valor String. Es un valor de tipo Class<String>. Tambien es un tipo. Entonces la sintaxis tiene que distinguir. No queremos in Ceylon que el compilador intente adivinar a menos que sea algo que pueda adivinar sin corner cases. (En Java tenemos que escribir String.class para referirse a la clase, pero en Ceylon no.)

Si, podemos escribir en Ceylon:

if (is Integer|Long num) { ...  }
case (is Integer|Long) { ...  }
catch (MyException|YourException e) { ...  }

Tambien podemos escribir:

if (is Sized&Category obj) { ...  }

etc.

Imagen de Sr. Negativo

Ceylon en "español"

@Gavin King "hablando en español" ... me parece muy bien (+1)

Que bueno que se involucre en este sitio y que aporte su conocimiento. No he usado Ceylon, se ve muy parecido a Groovy y Python... por lo pronto ya lo descarge

0_o

@Mr(-) Excepto que Ceylon es

@Mr(-) Excepto que Ceylon es de un "extremo tipado seguro" cosa que esta muy lejos de Groovy y Python. Si te refieres a la fluidez del lenguaje, entonces creo que es de los mejores cumplidos que puede haber.

:)

Imagen de ezamudio

Tipado

Así es, una diferencia crucial con Groovy/Python es el sistema de tipos. Ceylon es un lenguaje de tipado estático, con mucho énfasis en la seguridad de tipos.

Tipos

Gavin King ha hecho una excelente convención de código al usar nombre completos en vez de letras sueltas para los tipos, List<Element> es muchísimo mejor que List<T> y cuando la cosa se complica porque hay muchos tipos genericos mezclados se vuelve todavia mejor. Yo estoy haciendo eso mismo en Java y ese sencillo cambio vuelve legibles cosas ilegibles

Además, el sistema de tipos de Ceylon es una maravilla. No se si es menos poderoso que el de scala, pero es mucho más cómodo para usar y para leer. Ha demostrado que se puede ser mejor que Java sin ser mas oscuro que Java.

Aparte de eso, voya defender un poco a Java...Si yo necesitara una lista que puede tener enteros o strings, no usaria List<Object> sinó List<Either<String, Integer>>, creando una clase Either que finja ser un union type....es mas, no se si Ceylon no hace algo asi cuando una escribe Sequence<String|Integer>. ¡Se puede ser mas expresivo en Java, pero es mucho mas molesto y hay que escribir un montón!

El sistema de tipos de Ceylon me encanta....si logran arreglar (reificar) los generics, es perfecto. Y si no, igual ya es excelente.

Imagen de ezamudio

reificar

Reificar generics es un objetivo a corto plazo, y según cuentan las malas lenguas, ya mero está.

En Ceylon la unión de tipos no se simula con un Either. Recuerda que los generics se van a la goma en el bytecode, sólo sirven para que el compilador pueda hacer validaciones adicionales. El compilador de Ceylon sabe de la unión e intersección de tipos y simplemente si defines una lista que recibe uno o dos o tres tipos, los considera en las validaciones (porque no nada más hay de dos, puedes hacer List<Integer|String|URL|File> si quieres).

style="display:inline-block;width:728px;height:90px"
data-ad-client="ca-pub-5164839828746352"
data-ad-slot="7563230308">