Varianza en Ceylon

Bueno pues después del artículo maratónico acerca de varianza en Java, ahora quiero explicar cómo se implementó esto en Ceylon. Para ello vamos a usar los mismos ejemplos, de modo que quede clara la comparación.

Lo primero son las tres clases para el ejemplo, y la primera función:

class A() {}
class B() extends A() {}
class C() extends B() {}

B f(B param) => C();

Hasta aquí, todo funciona exactamente igual que en Java:

B algo;
A a = f(algo); //OK
B b = f(algo); //OK
C c = f(algo); //Error

f(A()); //ERROR
f(B()); //OK
f(C()); //OK

En Ceylon también existe la covarianza en los tipos de retorno, similar a lo que se introdujo en Java 5:

class Padre() {
  shared default A metodo() => A();
}
class Hijo() extends Padre() {
  shared actual C metodo() => C();
}

Con los parámetros no se puede hacer algo similar. Ceylon no tiene sobrecarga de métodos, pero no tiene contravarianza en los parámetros; por lo tanto, al refinar un método de un supertipo, los parámetros deben ser exactamente del mismo tipo que en la declaración original, o el compilador emite un error.

Ahora, entremos a la parte interesante, con los parámetros de tipo, con el contenedor simple y una función que lo utilice:

class Cont<Tipo>(shared Tipo t) {}

Cont<B> g(Cont<B> param) {
  return Cont(B());
}

Cont<A> a = g(algo);
Cont<B> b = g(algo);
Cont<C> c = g(algo);

Al igual que en Java, solamente la segunda llamada es válida. Pero al menos, el error en Ceylon es bastante más entendible:

error: specified expression must be assignable to declared type of 'a': 'Cont<B>' is not assignable to 'Cont<A>'
Cont<A> a = g(algo);
         ^
error: specified expression must be assignable to declared type of 'c': 'Cont<B>' is not assignable to 'Cont<C>'
Cont<C> c = g(algo);
         ^

En Ceylon, la varianza se define en el sitio de declaración. Es decir, donde se declara el parámetro de tipo:

class Cont<out Tipo>(shared Tipo t) {}

La sintaxis es muy simple: se usa out para marcar un parámetro de tipo como covariante, o in para marcarlo como contravariante. Si no se usa ninguno de los dos, es invariante. Y ahora, sin tener que modificar ese código, ya solamente la tercera línea falla:

Cont<A> a = g(algo); //OK
Cont<B> b = g(algo); //OK
Cont<C> c = g(algo); //ERROR

Tal vez recuerden que en Java, pudimos forzar a la tercera línea marcándola como contravariante, para que compilara. En Ceylon esto no es posible, sólo se puede definir varianza en sitio de uso con parámetros de tipo invariantes. Lo bueno es que el error es bastante claro:

error: type parameter is not declared invariant: 'Tipo' of 'Cont'
Cont<in C> c = g(algo);
    ^
error: specified expression must be assignable to declared type of 'c': 'Cont<B>' is not assignable to 'Cont<C>'
Cont<in C> c = g(algo);
            ^

Ahora vamos a ver qué pasa con los parámetros, pero ya de una vez con el Cont covariante:

g(Cont(A()));
g(Cont(B()));
g(Cont(C()));

Ceylon no tiene el operador diamante de Java... tiene el operador invisible. Simplemente no es necesario indicar el argumento de tipo porque el compilador lo puede inferir: la primera llamada es a un Cont<A> por ejemplo.

La primera línea, por cierto, nos da error; esto es porque el parámetro de b es un Cont<B>, por lo que se le puede pasar un Cont con B o C. Recuerden que el parámetro de tipo de Cont fue declarado covariante, por lo que el parámetro Cont<B> significa que acepta un Cont con B o cualquier subtipo de B.

Para contravarianza podemos usar un ejemplo concreto, en vez de imaginarnos cosas raras. En Ceylon hay varias interfaces que usan contravarianza; dos de ellas con Category y Comparable. Estos son unos fragmentos de las mismas:

shared interface Category<in Element=Object>
        given Element satisfies Object {
    shared formal Boolean contains(Element element);
}

shared interface Comparable<in Other> of Other
        given Other satisfies Comparable<Other> {
    shared formal Comparison compare(Other other);
}

Aprovecho para mencionar varias cosas que se ven aquí y que pueden llamarles la atención:

  • El parámetro de tipo Element de Category es contravariante, y por default será Object. Los parámetros de tipo en Ceylon pueden tener defaults.
  • Category.Element tiene definido un límite: given Element satisfies Object. Es bastante obvio por lo que dice, pero en caso que no: significa que cualquier argumento de tipo que quieran usar para Element, debe ser Object (o un subtipo de Object). Es decir, no acepta nulos.
  • El parámetro de tipo Other de Comparable es un self type (honestamente no sé cómo traducir esto). Es la parte que dice of Other y, de manera muy simplificada, representa un tipo concreto de Comparable (alguna implementación de esa interfaz). Sirve para limitar a que una implementación de Comparable solamente pueda tener como argumento de tipo al mismo tipo (p.ej. String sólo puede ser Comparable<String>; si no tuviera esa restricción, se podría hacer que String fuera Comparable<Float> cosa que no tiene sentido, sin embargo es posible en Java).

¿Por qué son contravariantes estas interfaces? Consideren esto:

interface X satisfies Comparable<X> {}
interface Y satisfies X & Comparable<Y> {}

Esto compila sin ningún problema porque Other es contravariante, lo cual permite a Y ser un Comparable<Y> a pesar de que ya es un X lo cual la hace un Comparable<X>. Si Comparable fuera invariante, entonces el compilador arrojaría este error al compilar Y:

type 'Y' has the same parameterized supertype twice with incompatible type arguments: 'Comparable<X> & Comparable<Y>'

La clave es incompatible type arguments; son incompatibles porque el parámetro de tipo es invariante. Pero al hacerlo contravariante, entonces Comparable<Y> significa que es Comparable con Y o cualquier supertipo de Y, y eso lo hace compatible con Comparable<X>.

Pero, este problema se resolvió en la definición de Comparable Los usuarios de esa interfaz (es decir, quienes programen en Ceylon) no tienen que preocuparse por eso. Una clase que satisfaga la interfaz Y sólo tiene que implementar compare de esta forma:

shared actual Comparison compare(X other) { ... }

Y se puede implementar una función de este tipo y luego invocarla pasándole argumentos de tipos dispares:

Comparison comparar<Algo>(Algo a, Algo b)
    given Algo satisfies Comparable<T>
  => a.compare(b);

comparar(ClaseQueImplementaX(), ClaseQueImplementaY());

En cambio, los usuarios de Comparable en Java sí tienen que preocuparse por este tipo de cosas. Este código no compila en Java:

interface X extends Comparable<X> {}
interface Y extends X, Comparable<Y> {}

La única forma de hacer que compile es así:

interface X extends Comparable<X> {}
interface Y extends X {}

Y para implementar ese método de comparación, hay que poner la contravarianza en el método:

<T extends Comparable<? super T>> void comparar(T a, T b) {
  a.compareTo(b);
}

Ahora simplemente díganme: ¿Cuántos de ustedes han escrito alguna vez un método con una firma similar? Es más: ¿Cuántos de ustedes han visto siquiera un método así? Seguramente muy pocos, y esto si de entrada la covarianza y sobre todo la contravarianza no son conceptos fáciles de entender, su uso en Java es bastante complicado. Por eso es una característica importante del sistema de tipos de Ceylon.

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.
Imagen de echan

Si es una decisión consciente

Si es una decisión consciente restringir la varianza en Ceylon me parace acertado, porque la mejor forma de no tener problemas de varianza es evitando la varianza.

Lo que quiero decir es que si la herencia y los genericos chocan y crean varianza es porque ambos conceptos tratan de resolver el problema de la abstracción pero de forma diferente. La pregunta es se puede usar solo una de las dos?

En Java tenemos
a) Polimorfismo ad-hoc con herencia, sobrecarga y dispatch dinámico
b) Polimorfismo parametrico con generics

y en Ceylon? Segun entiendo polimorfismo paramétrico es la mejor opción no?

Imagen de ezamudio

las dos

No hay sobrecarga de métodos en Ceylon, pero obviamente sí hay herencia, de hecho hay herencia múltiple porque las interfaces pueden tener métodos concretos. Por lo tanto, no hay polimorfismo ad-hoc, es polimorfismo de subtipos (o polimorfismo, a secas).

Si tienes una clase C<T> así, invariante, entonces puedes usar varianza en sitio de uso igual que en Java, por ejemplo void f(C<in Algo> x) o C<out Object> m(). Pero, si el parámetro de tipo ya es covariante o contravariante, entonces no se puede definir varianza en sitio de uso (que es lo que mostré en el ejemplo arriba). A eso me refiero cuando hablaba de la restricción de varianza.

Do you know ...

Do you know...

▲ LOL

Fuente: New Features in C# 2010: Covariance and Contravariance and List ← ¿.NET? ¡Por favor, no le digan a mis amigos!