====================================== Unión, intersección y tipos enumerados ====================================== Este es la octava parada en el tour de Ceylon. En la parada anterior aprendimos acerca de los alias y la inferencia de tipos. Continuemos explorando el sistema de tipos de Ceylon. En este capitulo, vamos a discutir los cercanos tópicos de tipos unión e intersección y los tipos enumerados. En esta área, el sistema de tipos de Ceylon trabaja un poco distinto a otros lenguajes de tipos estáticos. ------------------------------------------ Reduciendo el tipo de un objeto referencia ------------------------------------------ En cualquier lenguaje con subtipos hay ocasiones en las que necesitas realizar reducción con conversiones. En demasiados lenguajes de tipos estáticos, esta es la segunda parte del proceso. Por ejemplo, en Java primero probamos el tipo del objeto usando el operador `instanceof`, y entonces intentar reducirlo usando un `typecast` estilo C. Esto es un poco curioso, desde que estos no son virtualmente buenos usos para `instanceof` que no involucra una conversión inmediata al tipo que fue probado, y el cambio de tipo sin probar el tipo es peligrosamente un tipo no seguro. Como puedes imaginar, Ceylon, con su énfasis sobre los tipos estáticos, hace las cosas diferente, Ceylon no tiene un cambio de tipos al estilo de C. En ves de ellos, debemos de probar y reducir el tipo de la referencia de un objeto en un solo paso, usando la construcción especial `if (is ...)`. Esta construcción es muy parecida a `if (exists ...)` y `if (nonempty ...)`, las cuales conocimos tempranamente. .. code-block:: ceylon void PrintIfPrintable(Object obj) { if (is Printable obj){ obj.printObj(); } } También hay una construcción especial `if (!is ...)` que es practica algunas veces. La declaración `switch` puede ser usada de una manera similar: .. code-block:: ceylon void switchingPrint(Object obj) { switch(obj) case (is Hello) { obj.printMsg(); } case (is Person) { print(obj.firstName); } else { print(obj.string); } } Estas construcciones nos protegen de escribir inadvertidamente codigo que pueda causar una `ClassCastException` en Java, justo como `if (exists ...)` nos protege de escribir codigo que pueda causar `NullPointerException`. La construcción `if (is ...)` actualmente reduce el código a un tipo intersección. ------------------ Tipos intersección ------------------ Una expresión es asignable a un tipo intersección, escrito X&Y, si este es asignable a ambos X&Y. Por ejemplo, desde que `Tuple` es un subtipo de `Iterable` y de `Correspondence`, la tupla tipo `[String,String]` es también un subtipo de la intersección. `Iterable&Correspondence`. El supertipo de un tipo intersección incluye todos los supertipos de cada tipo interceptado. Entonces, el siguiente código esta bien tipado: .. code-block:: ceylon Iterable&Correspondece string = ["Hello", "world"]; String? str = strings.get(0); Integer size = strings.size; Ahora considerar este código, para ver el efecto de `if (is ...)`: .. code-block:: ceylon Iterable strings = ["hello","world"]; if (is Correspondence strings) { //Aqui string tiene el tipo //Iterables & Correspondence String? str = strings.get(0); Integer size = strings.size(); } Dentro del cuerpo de la construcción `if`, `strings` tiene el tipo `Iterable&Correspondence`, así podemos llamar las operaciones de ambos, `Iterable` y `Correspondence`. ----------- Tipos unión ----------- Una expresión es asignable a un tipo unión, escrito X|Y, si este es asignable a cualquiera X o Y. El tipo X|Y es siempre un supertipo de ambos X y Y. El siguiente código esta bien tipado: .. code-block:: ceylon void printType(String|Integer|Float val) { ... } printType("hello"); printType(69); printType(-1.0); Pero, ¿qué operaciones tiene un tipo como `String|Integer|Float`? ¿Cuales son sus supertipos? Bueno, la respuesta es muy intuitiva: T es un supertipo de X|Y si y solo si es un supertipo de X y de Y. El compilador de Ceylon determina esto automáticamente. Así el siguiente código esta bien tipado. .. code-block:: ceylon Integer|Float x= -1; Number num = x; // number es un supertipo de ambos Integer y Float String|Integer|Float val = x; // String|Integer|Float es un supertipo // de Integer|Float Object obj = val; //Object es un supertipo de String, Integer y Float. Sin embargo, el siguiente código no esta bien tipado, desde que `Number` no es un supertipo of `String`. .. code-block:: ceylon String|Integer|Float x = -1; Number num = x; //compile error: String is not a subtype of Number Por supuesto, es muy común reducir una expresión de tipo unión usando una declaración `switch`. Usualmente el compilador de Ceylon nos fuerza a escribir una clausula `else` en un `switch` para recordarnos que puede haber casos adicionales que no han sido manejados. Pero si agotamos todos los casos de un tipo unión, el compilador nos permitirá dejar fuera la clausula `else`. .. code-block:: ceylon void printType(String|Integer|Float val) { switch (val) case (is String) { print("String: ``val``"); } case (is Integer) { print("Integer: ``val``"); } case (is Float) { print("Float: ``val``"); } } Un tipo unión es una especia de tipo enumerado. --------------- Tipos Enumerado --------------- Algunas veces es útil que este disponible realizar el mismo tipo de cosas con subtipos de una clase o interfaz. Primero, necesitamos explícitamente enumerar los subtipos de el tipo usando la clausula `of`: .. code-block:: ceylon abstract class Point() of Polar | Cartesian { //... } (Esto crea un punto en la versión de Ceylon de lo que llaman en la comunidad de programación funcional un tipo "algebraico" o "sum". Ahora el compilador no nos permitirá declarar subclases adicionales a `Point`, así el tipo unión `Polar|Cartesian` es exactamente el mismo tipo como `Point`. Ademas podemos escribir declaraciones `switch` sin la clausula `else`: .. code-block:: ceylon void printPoint(Point point) { switch (point) case (is Polar){ print("r = " + point.radius.string); print("theta = " + point.angle.string); } case (is Cartesian) { print("x = " + point.x.string); print("y = " + point.y.string); } } Ahora, es usualmente considerado una mala practica escribir largas declaraciones de `switch` que manejen todos los subtipos de un tipo. Esto crea un código no extensibles. Agregar una nueva subclase a `Point` significa romper todas las declaraciones que agotaron los subtipos. En programación orientada a objetos, tratamos de refactorizar construcciones como esta usando un método abstracto de la superclase que es sobrescrita apropiadamente por subclases. Sin embargo, esta es una clase de problema donde este tipo de refactorisación no es apropiado. En muchos lenguajes de programación orientados a objetos, estos problemas son usualmente resueltos usando el patrón "visitor". ######## Visitors ######## Consideremos las 3 siguientes implementaciones visitor: .. code-block:: ceylon abstract class Node() { shared formal void accept(Visitor v); } class Leaf(shared object element) extends Node() { accept (Visitor v) => v.visitLeaf(this); } class Branch(shared Node left, shared Node rigth) extends node() { accept(Visitor v) => v.visitBranch(this); } interface Visitor { shared formal void visitLeaf(Leaf l); shared formal void visitBranch(Branch b); } Podemos crear un método que imprima las tres implementando la interfaz `Visitor`: .. code-block:: ceylon void printTree(Node node) { object printVisitor satisfies Visitor { shared actual void visitLeaf(Leaf leaf) { print("Found a leaf: ``leaf.element``!"); } shared actual void visitBranch(Branch branch){ branch.left.accept(this); branch.rigth.accept(this); } } node.accept(printVisitor); } Note que el código de `printVisitor` luce como la delaración de `switch`. El deberá explícitamente enumerar todos los subtipos de `Node`. Estará "roto" si agregamos un nuevo subtipo de `Node` a la interfaz `Visitor`. Esto es correcto, y es el comportamiento deseado; "roto" significa que el compilador nos permitirá conocer que tenemos que actualizar nuestro código para manejar un nuevo subtipo. En Ceylon podemos lograr el mismo efecto, con menos verbosidad, enumerando los subtipos de `Node` en su definición y usando un switch: .. code-block:: ceylon abstract class Node() of Leaf | Branch {} class Leaf(shared Object element) extends Node() {} class Branch(shared Node left, shared Node right) extends Node() {} Nuestro método `print()` es ahora mucho mas simple, pero aun tiene el mismo comportamiento deseado de "romperse" cuando un nuevo subtipo de `Node` es agregado. .. code-block:: ceylon void printTree(Node node) { switch (node) case (is Leaf) { print("Found a leaf: ``node.element``!"); } case (is Branch) { printTree(node.left); printTree(node.right); } } ################### Interfaz enumeradas ################### Ordinariamente, Ceylon no nos permitirá usar tipos interfaz como un `case` de `switch`. Si `File,Directory,` y `Link` son interfaces, no podemos escribir tal cual: .. code-block:: ceylon File|Directory|Link resources = ...; switch (resources) case (is File) { ... } case (is Directory) { ... } //compile error: cases are not disjoint case (is Link) { ... } //compile error: cases are not disjoint El problema es que los casos no están disjuntos. Podemos tener una clase que satisfaga ambos `File` y `Directory` y entonces no saber que rama ejecutar. (En todos nuestros ejemplos anteriores, nuestros `case` referenciaban a tipos que eran probablemente disjuntos, debido a que eran clases, que soportan únicamente herencia simple.) Hay una solución, a pesar de todo. Cuando una interfaz tiene subtipos enumerados, el compilador fuerza a estos subtipos a estar disjuntos. Así que si definimos la siguiente interfaz enumerada: .. code-block:: ceylon interface Resource of File|Directory|Link { ... } Entonces la siguiente declaración es un error: .. code-block:: ceylon class DirectoryFile() satisfies File&Directory {} //compile error: File and Directory are // disjoint types Ahora esto es aceptado por el compilador: .. code-block:: ceylon Resource resource = ...; switch (resource) case (is File) { ... } case (is Directory) { ... } case (is Link) { ... } El compilador es muy inteligente cuando razón acerca de las desuniones y agotamiento. Por ejemplo, esto es aceptable: .. code-block:: ceylon Resources resource = ...; switch (resource) case (is File|Directory) { ... } case (is Link) { ... } Como este, asumiendo la anterior declaración de `Resource`: .. code-block:: ceylon File|Link resource = ... ; switch (resource) case (is File) { ... } case (is Link) { ... } Si estas interesando en conocer mas acerca de esto, `lee esto `_ ##################### Instancias enumeradas ##################### Ceylon no tiene algo exactamente como la declaración `enum` de Java. Pero emular el efecto usando la clausula `of`. .. code-block:: ceylon abstract class Suit(String name) of hearts|diamons|clubs|spades {} object hearts extends Suit("hearts") {} object diamonds extends Suit("diamonds") {} object clubs extends Suit("clubs") {} object spades extends Suit("spades") {} Es permitido usar los nombres de las declaraciones `object` en la clausula `of`. Ahora podemos agotar todos los casos de `Suit` en un `switch`: .. code-block:: ceylon void printSuit(Suit suit) { switch (suit) case (hearts) { print("Heartzes"); } case (diamonds) { print("Diamondzes"); } case (clubs) { print("Clidubs"); } case (spades) { print("Spidades"); } } Note que estos casos son casos de valor, no casos de tipo `case (is ...)`. Ellos no necesitan reducir el tipo `Suit` Así es esto es un poco mas verboso que el `enum` de Java, pero también es algo mas flexible. Para mas ejemplos prácticos, revisa la definición de `Boolean` y `Comparison` en el modulo del lenguaje. ########### Aun hay mas ########### En la siguiente estación veremos el `sistema genérico de tipos` en profundidad.