======== Generics ======== Esta es la novena parte de el tour de Ceylon. El la parada anterior cubrimos os tipos unión, intersección y enumerados. En esta parte vamos a ver acerca de los tipos genéricos. Herencia y subtipos son poderosas herramientas para la abstracción sobre los tipos. Pero esta herramienta tiene sus limitantes. No nos ayuda a expresar contenedores de tipos genéricos como colecciones. Para este problema necesitamos tipos parametrizados. Ya hemos visto muchos tipos parametrizados - por ejemplo, iterables, secuencias y tuplas - pero ahora vamos a explorar esto a detalle. -------------------------- Definiendo tipos genéricos -------------------------- Programar con tipos genéricos es una de las partes mas difíciles en Java. Esto es aun verdad, en cierta parte en Ceylon. Pero debido a que el lenguaje Ceylon y el SDK fueron diseñados para generics desde cero, Ceylon esta diseñado para aliviar demasiados aspectos dolorosos de modelo de Java. Justo como en Java, únicamente tipos y métodos puede declarar parámetros de tipo. También como en Java, los parámetros de tipo son antes que los parámetros ordinarios, y encerrados entre corchetes angulares. .. code-block:: ceylon shared interface Iterator { ... } .. code-block:: ceylon shared class Singleton(Element element) extends Object() satisfies [Element+] given Element satisfies Object { ... } .. code-block:: ceylon shared Value sum({Value+} values) given Value satisfies Summable { ... } .. code-block:: ceylon shared Item>[] zip({Key*} keys, {Item*} items) given Key satisifes Object given Item satisfies Object { ... } Como puedes ver, la convención en Ceylon es usar nombres significativos para los parámetros de tipo (en otros lenguajes de programación la convención es usar letras). Un parámetro de tipo puede tener un parámetro por defecto. .. code-block:: ceylon shared interface Iterable ... ------------------- Argumentos de tipo ------------------- A diferencia de Java, siempre necesitamos especificar el parámetro tipo en la declaración de tipo (No hay tipos ``raw`` en Ceylon. El siguiente código no compilara: .. code-block:: ceylon Iterator it = ...; //error: missing type agmuente to parameter Element of Iterable En vez de ello, necesitamos proveer el argumento tipo como esto: .. code-block:: ceylon Iterator it = ...; Por otro lado, no necesitamos explícitamente especificar el tipo de los argumentos en muchas invocaciones de métodos o instanciaciones de clases. No necesitamos usualmente tener que escribir esto: .. code-block:: ceylon Array strings = array { "Hello", "World" }; {String>*} things = entries(strings); En vez de ello, es posible inferir los tipos de los argumentos desde argumentos ordinarios. .. code-block:: ceylon value string = array { "Hello", "World" }; //tipo: Array value things = entries(strings); //tipo: Iterable> El algoritmo de inferencia de tipos generic esta ligeramente involucrado, así que deberás referirte a las especificaciones del lenguaje para una definición completa. Pero esencialmente lo que pasa es que Ceylon infiere el tipo de un argumento combinando los tipos de los correspondientes argumentos usando unión en el caso de un parámetro de tipo covariante o intersección en el caso de un parámetro de tipo contravariante. .. code-block:: ceylon value points = array { Polar(pi/4, 0.5), Cartesian(-1.0, 2.5) }; // tipo: Array value entries = entries(points); //tipo: Entries Si un parámetro de tipo tiene un argumentos por defecto, esta permitido dejarlo fuera cuando subministremos la lista de argumentos de tipo. Entonces ``Iterable`` significa ``Iterable``. --------------------------- Covarianza y contravarianza --------------------------- Ceylon elimina una de la partes de los generics de Java que hacían realmente difíciles las cosas: tipos ``wildcard``. Los tipos ``wildcard`` fueron la solución al problema de covarianza en un sistema de tipos genérico en Java. Conozcamos la idea de covarianza, y entonces podremos ver como trabaja la covarianza en Ceylon. Esto comienza con la intuitiva expectación de que una colección de ``geeks`` es una colección de ``Person``. Esta es una intuición razonable, pero si la colección mutan, esto cambiara a ser incorrecto. Consideremos la siguiente posible definición de ``Collection``: .. code-block:: ceylon interface Collection { shared formal Iterator iterator(); shared formal void add(Element x); Y vamos a suponer que ``Geek`` es un subtipo de ``Person``. La expectación intuitiva es que el siguiente código deberá de funcionar: .. code-block:: ceylon Collection geeks = ... ; Collection people = geeks; //compile error for (person in people) { ... } Este código es, francamente, perfectamente razonable tomado enserio. Aun en ambos Ceylon y Java, este código resulta en un error en tiempo de compilación en la segunda linea, donde ``Collection`` es asignado a una ``collection``. ¿Por qué? Bueno, debido a que si permitimos la asignación, el siguiente código deberá también compilar: .. code-block:: ceylon Collection geeks = ...; Collection people = geeks; //compile error people.add( Person("Fonzie") ); ¡No podemos permitir que el código de ``Fonzie`` sea un ``Geek``! En otras palabras, diremos que ``Collection`` es invariante en ``Element``. O, cuando no estemos tratando de impresiones gente con terminología confusa, podremos decir que ``Collection`` producen ambos a través del métodos ``iterator()`` y consume a través del método ``add()`` el tipo ``Element``. Aquí es donde Java queda fuera y se dirige a abajo por el agujero del conejo, exitosamente usando ``wildcards`` para disputar un tipo covariante o contravariante de un tipo invariante, pero también exitosamente dejando confusos a todos. No vamos a seguir a Java hasta el fondo del agujero. En vez, vamos a refactorizar ``Collection`` en una interfaz puramente ``Producer`` y en una puramente ``Consumer``: .. code-block:: ceylon interface Producer { shared formal Iterator iterator(); } interface Consumer { shared formal void add(Input x); } Note que hemos anotado los parámetros de tipo de estas interfaces. - La anotación ``out`` especifica que ``Producer`` es covariante en ``Output`` Esto es que produce una instancia de ``Output``, pero nunca consume instancias de ``Output``. - El anotación ``in`` especifica que ``Consumer`` es una contravariante de ``Input``. Esto es que consume una instancia ``Input``, pero nunca produce una instancia de ``Input``. El compilador de Ceylon valida el esquemas de la declaración del tipo y se asegura que la anotaciones de varianza estén satisfechas. Si tratas de declarar un método ``add()`` en ``Producer`` dara como resultado un error de compilación. Si tratas de declarar un método ``iterate()`` en ``Consumers`` obtendrás en error de compilación similar. Ahora, veamos que sacamos de esto: - Desde que ``Producer`` es covariante en su parámetro de tipo ``output``, y desde que ``Geek`` es un subtipo de ``Person``, Ceylon nos permite asignar ``Producer`` a ``Producer``. - Además, desde que ``Consumer`` es una contravariante en su parámetro de tipo ``Input``, y desde que geek es un subtipo de ``Person``, Ceylon nos permite asignar ``Consumer`` a ``Consumer``. Y así poder definir nuestra interfaz ``Collection`` como una mezcla de ``Producer`` con ``Consumer``. .. code-block:: ceylon interface Collection satisfies Producer & Consumer {} Note que ``Collection`` permanece invariante en ``Element``. Si tratamos de agregar una anotación de varianza a ``Element`` en ``Collection`` dará como resultado un error de compilación, debido a que la anotación deberá contradecir la anotación de varianza de cualquiera ``Producer`` o ``Consumer``. Ahora el siguiente código finalmente compila: .. code-block:: ceylon Collection geeks = ...; Producer people = geeks; for (person in people) { ... } El cual coincide con nuestra intuición original. El siguiente código también compila: .. code-block:: ceylon Collection people = ...; Consumer geekConsumer = people; geekConsumer.add( Geek("James") ); Que es también intuitivamente correcto - "James" deberá ser una persona. Hay dos elementos adicionales a la definición de covarianza y contravariaza: - ``Producer`` es un supertiopo de ``Producer`` para cualquier tipo T, y - ``Consumer`` es un supertipo de ``Consumer`` para cualquier tipo T. Estas invariantes pueden ser útiles si necesitas abstraer todos los ``Producers`` o todos los ``Consumers``. (Nota, sin embargo, si ``Producer`` declaro restricciones obligatorias para tipos en ``Output``, entonces ``Producer`` no deberá ser un tipo legal.) No gastaras mucho tiempo escribiendo tus propias colecciones, desde que Ceylon SDK deberá próximamente tener un poderoso framework para construir colecciones. Pero aun deberás apreciar el enfoque de Ceylon a la convarianza como un usuario de los tipos colección incorporados. #################################################### Covarianza y contravarianza con unión e intersección #################################################### Hay un conjunto de relaciones interesantes que surge cuando introducimos los tipos unión e intersección en la pintura. Primero, consideremos un tipo covariante como ``List``. Entonces para cualquier tipo ``X`` y ``Y``: - ``List|List`` es un subtipo de ``List``, y - ``List&List`` es un supertipo de ``List``. Después, consideremos un tipo contravariante como ``Consumer``. Entonces para cualquier tipo ``X`` y ``Y``: - ``Consumer|Consumer`` es un subtipo de ``Consumer``, y - ``Consumer&Consumer`` es un supertipo de ``Consumer``. Esto es valioso volveremos a esta sección mas adelante, y trataremos de desarrollar alguna intuición acerca del por que estas relaciones son correctas y que significan. No gastes tu tiempo en esto por ahora. Tenemos cosas mas importantes que hacer. ################### Generics y herencia ################### Considere las siguientes clases: .. code-block:: ceylon class LinkedList() satisfies List { ... } class LinkedStringList() extends LinkedList() satisfies List { ... } Este tipo de herencia es ilegal en Java. Una clase no puede heredar el mismo tipo mas de una vez, con diferentes argumentos de tipo. Podemos decir que Java suporta únicamente ``single instantiation inheretance``. Ceylon es menos restrictivo en este aspecto. El código anterior es perfectamente legal si (y solo si) la interfaz ``List`` es covariante en sus parámetros de tipo ``Element``, que es, declarado como esto: .. code-block:: ceylon inteface List { ... } Diremos que Ceylon cuenta con ``principal instantiation inheritance``. Incluso el siguiente código es legal: .. code-block:: ceylon interface ListOfSomething satisfies List { } interface ListOfSomthingElse satisfies List {} class MyList() satisfies ListOfSomething & ListOfSomethingElse { ... } Entonces el siguiente código es lagal y bien tipado: .. code-block:: ceylon List list = MyList() Por favor hagamos una pausa aquí, y toma tu tiempo para notar que tan ridículamente impresionante es esto. Nosotros nunca mencionamos explícitamente que ``MyList()`` fue una ``List``. El compilador solo lo dedujo por nosotros. Note que cuando heredaste el mismo tipo mas de una vez, tu tal vez necesites refinar algunos de sus miembros, en orden para satisfacer todas las firmas heredadas. No te preocupes el compilador te lo notificara y te obligara a hacerlo. ############################## Restricciones en tipos generic ############################## Es muy común, cuando estamos escribiendo un tipo parametrizado, queremos invocar un método o evaluar un atributo a instancias del parámetro de tipo. Por ejemplo, si estamos escribiendo un tipo parametrizado ``Set``, necesitamos ser capaces de comparar instancias de ``Element`` usando ``==`` para ver si cierta instancia de ``Element`` es contenida en el ``Set``. Desde que ``==`` esta definido para expresiones de tipo ``Object`` necesitamos alguna manera de asegurar que ``Element`` es un subtipo de ``Object``. Este es un ejemplo de una *restricción de tipo* - de hecho, este es un ejemplo del caso mas común de restricción de tipo, un *upper bound*. .. code-block:: ceylon shared class Set(Element* elements) given Element satisifes Object { ... shared Boolean contains(Object obj) { if (is Element obj){ return obj in bucket(obj.hash); } else { return false; } Un argumento de tipo a ``Element`` deberá ser un subtipo de ``Object``. .. code-block:: ceylon Set set1 = Set("C", "Java", "Ceylon"); //ok Set set2 = Set("C", "Java", "Ceylon", null); //compile error En Ceylon, un parámetro de tipo genérico es considerado un tipo propio, así una restricción de tipo luce mas a una declaración de una clase o interfaz. Esta es otra forma en la que Ceylon es mas regular que otros lenguajes parecidos a C. En futuras versiones de Ceylon, después de la 1.0, también introduciremos soporte para varios tipos adicionales de restricciones de tipos genéricos. Podrás encontrar mas detalles en las especificaciones del lenguaje. ###################################### Tipos genéricos totalmente cosificados ###################################### La causa principal de muchos problemas cuando trabajamos con tipos genéricos en Java es el borrado de tipos. Los parámetros y argumentos de tipo son descartados por el compilador y simplemente no están disponibles en tiempo de ejecución. Así el siguiente, perfectamente sensible, fragmento de código no deberá compilar en Java; .. code-block:: ceylon if (is List list) { ... } if (is Element obj) { ... } (Donde elemento es un parámetro de tipo genérico.) El sistema de tipos de Ceylon ha cosificado los argumentos de tipo genérico, Como Java, el compilador de Ceylon lleva acabo limpieza, descartando parámetros de tipo desde el esquema de el tipo genérico. En la plataforma de JavaScript, los tipos son descartados cuando se produce el código de JavaScript. Pero a diferencia de Java, los parámetros de tipo son cosificados(disponibles en tiempo de ejecución). Los tipos son incluso cosificados cuando se ejecutan en una máquina virtual de Java. Así los fragmentos de código anteriores compilan y funciones como se esperan en ambas plataformas. Una vez hemos terminado de implementar el metamodelo, incluso podrás seras capaz usar reflexión para para descubrir el argumento de tipo de una instancia de un tipo genérico. Ahora por supuesto, argumentos de tipos genéricos no son revisada para seguridad de tipos a nivel de la máquina virtual cuando se esta ejecutando, pero esto no es estrictamente necesario desde que el compilador desde que el compilarlo ya ha revisado la solidez del código. **Nota de implementación** En el release M5 no hemos tenido tiempo de implementar algunas optimizaciones importantes relacionadas a genéricos cosificados. Entonces, tal vez experimentes algunos problemas usando tipos genéricos en este release. No te preocupes que resolveremos estos problemas al tiempo en que lancemos la versión de Ceylon 1.0. (¡Por favor haz nos saber tus experiencias!) ########### Aun hay mas ########### Ahora estamos listos para mirar una característica muy importante de Ceylon: ``modularidad``.