This page looks best with JavaScript enabled

C#9 – Type classes y extensiones

 ·  ☕ 8 min  ·  ✍️ eiximenis

Nota: Este post ha sido importado de mi blog de geeks.ms. Es posible que algo no se vea del todo "correctamente". En cualquier caso puedes acceder a la versión original aquí

Estaba yo revisando algunas de las nuevas características que quizá incorpore C# 9 y me he encontrado con la propuesta de type classes (shapes en la teminología de C#), que me parece bastante interesante y sobre la cual me gustaría hacer algunos comentarios 🙂

Un type class (voy a dejarlo en inglés) es a grandes rasgos la declaración de un conjunto de métodos. Parece ser que shape va a ser la palabra clave apra crear un type class:

public shape SGroup<T>
{
    static T operator +(T t1, T t2);
    static T Zero { get; }
}

A primera vista no parece que se diferencie mucho de una interfaz: un conjunto de métodos sin implementar. Pero observa que a diferencia de una interfaz un type class puede declarar métodos estáticos. Por supuesto, puede declarar también métodos de instancia.

Un type class no es un “tipo de datos” como sí lo es una interfaz, una clase o una estructura. No puedes declarar variables cuyo tipo sea un type class ni usarlos como valores de retorno o como parámetros. Entonces… ¿para qué sirve?

Restricciones de genéricos

Es imposible hacer en C# una función genérica que sume dos valores usando el operador de suma:

public T Add<T>(T t1, T t2) => t1 + t2;

Este código no compila. Claro que hay buenas razones para que no lo haga: Esta función solo funciona si el operador + está definido para el tipo T. Algunos tipos lo definen (como Int32 o String) mientras que muchos otros no. Así, como el compilador “no sabe que es T” se cura en salud. Bien hecho.

Sabemos que a los tipos genéricos les podemos aplicar restricciones. Así, la función anterior podría reescribirla como:

interface IAdd<T>
{
    T AddTo(T t1);
}
struct A : IAdd<A>
{
    public int Value { get; }
    public A(int value) => Value = value;
    public A AddTo(A a) => new A(Value + a.Value);
}
class Program
{
    public static T Add<T>(T t1, T t2)
        where T : IAdd<T> => t1.AddTo(t2);
    static void Main(string[] args)
    {
        var result = Add(new A(10), new A(20));
    }
}

Al forzar que el parámetro genérico de Add debe ser un tipo que implemente IAdd, entonces ya podemos usar los métodos definidos en la interfaz IAdd dentro de la función Add (como AddTo). Esta aproximación funciona bastante bien en algunos casos pero tiene dos problemas fundamentales:

  • No vas a poder usar nunca Add con tipos que no implementen IAdd: Por lo tanto olvídate de los tipos que no sean tuyos.
  • No hay acceso a métodos estáticos (de hecho las interfaces no pueden declararlos). Eso impide usar p. ej. operadores (que se definen siempre como un método estático).

Con esto en mente, volvamos a nuetro type class inicial:

public shape SGroup<T>
{
    static T operator +(T t1, T t2);
    static T Zero { get; }
}

Como dije antes eso no declara tipo alguno. Eso le indica al compilador qué cualquier clase que tenga el operador+ y una propiedad estática llamada Zero se puede considerar un SGroup.

voy a poder usar SGroup como restricción de tipo genérico:

public static AddAll<T>(T[] ts) where T : SGroup<T>
{
    var result = T.Zero;
    foreach (var t in ts) { result = result + t; }
    return result;
}

Como puedes ver el segundo problema (uso de métodos estáticos) se ha solucionado.

Ahora la duda es qué ocurre con el primero. Pues la realidad es que los tipos deben explícitamente declarar que implementan un type class. Al igual que en el caso de interfaces, no “basta” con poseer los métodos (no hay tipado estructural).

Así las clases podrán implementar un type class del mismo modo que implementan una interfaz. La diferencia está en que deberan implementar también los métodos estáticos. Obviamente, eso no nos soluciona el primer problema: los tipos existentes no implementaran el type class así que nos quedamos igual. Aquí es donde entra en juego un nuevo mecanismo que se añade al lenguaje:

Extensiones

Las extensiones son una mejora al mecanismo de métodos de extensión. Actualmente ya disponemos en C# de un mecanismo para agregar métodos a un tipo existente: los métodos de extensión. A nivel de lenguaje “básico” los métodos de extensión no añaden nada nuevo: son simplemente métodos estáticos añadidos a una clase estática. Nada nuevo, salvo que el compilador “nos deja llamarlos” como si fuesen métodos del tipo base. Pues bien, se propone dotar a las extensiones de un elemento del lenguiaje:

public extension IntGroup of int: SGroup<int>
{
 public static int Zero => 0;
}

Este código declara una extensión llamada IntGroup. Esta extensión implementa SGroup (el type class) y se aplica sobre el tipo int. Observa, y eso es importante, que la extensión no declara el operator+. ¿Por qué? La razón es que el tipo int ya tiene ese operador. La extensión simplemente extiende el tipo int para que implemente el type class. El type class pide un operator+ y una propiedad estática Zero. En este ejemplo el propio tipo proporciona el operator+ y la extensión le añade aquello que hace falta.

Hay que tener presente que **las extensiones pueden usarse independientemente de los type classes, **no es necesario que una extensión termine “implementando” un type class. Aunque, en muchos casos van a usarse para eso: ambas características tienen una gran convergencia. Pero las extensiones por si solo ya añaden valor, al permitir añadir propiedades y métodos estáticos a un tipo (algo imposible hasta ahora). Por lo que he entendido las extensiones permitirán que un tipo implemente un type class pero NO una interfaz. Como hasta ahora, las interfaces deben ser implementadas por el propio tipo.

Como se ha dicho, las extensiones pueden añadir propiedades y métodos de instancia. En este caso, se puede usar this exactamente igual que en una clase:

public shape SComparable<T>
{
    int CompareTo(T t);
}
public extension IntComparable of int : SComparable<int>
{
    public int CompareTo(int t) => this - t;
}

También se puede extender una interfaz para que implemente un type class. Incluso, si la interfaz ya posee los métodos, la extensión es trivial (observa como Comparable queda vacía, ya que IComparable ya contiene los métodos de SComparable):

public shape SComparable<T>
{
    int CompareTo(T t);
}
public interface IComparable<T>
{
    int CompareTo(T t);
}
public extension Comparable<T> of IComparable<T> : SComparable<T> ;

Cualquier tipo que implemente IComparable, implementa tambiém SComparable (gracias a la extensión vacía). Igual te preguntas entonces por qué narices usar SComparable. Pues muy fácil: si usas IComparable como restricción de genéricos solo aquellos tipos que lo implementan podrán ser usados y las extensiones no te podrán ayudar. Si usas el type class, los tipos que implementen el type class los podrás usar y para el resto podrás crear una extensión.

Extendiendo type classes

Rizando el rizo **podemos crear extensiones que extiendan un type class. **Eso nos permite añadir comportamiento a todos aquellos tipos que implementen (directamente o indirectamente vía una extensión) el type class. No es necesario que el resultado de extender un type class sea otro type class:

public extension Comparison<T> of SComparable<T>
{
    public bool operator ==(T t1, T t2) => t1.CompareTo(t2) == 0;
    public bool operator !=(T t1, T t2) => t1.CompareTo(t2) != 0;
    public bool operator > (T t1, T t2) => t1.CompareTo(t2) >  0;
    public bool operator >=(T t1, T t2) => t1.CompareTo(t2) >= 0;
    public bool operator < (T t1, T t2) => t1.CompareTo(t2) <  0;
    public bool operator <=(T t1, T t2) => t1.CompareTo(t2) <= 0;
}

Esta extensión añade todos esos operadores a cualquier tipo que implemente SComparable.

Type classes en ejecución

Por lo que se está diciendo, no parece que haya un soporte directo en el CLR para los type classes y que casi toda la magia estará en el compilador. Eso significa que en ejecución los type classes desaparecen y solo habrá interfaces y clases estáticas generadas por el compilador. Desconozco como eso afectará a reflection y como “persistirá” la información de un type class en un ensamblado.

¿Es eso tipado estructural?

El tipado estructural es un sistema de tipos en el cual la validez de un tipo como parámetro, se basa únicamente en lo que se use de dicho tipo. Cualquier tipo que tenga definidos los métodos, propiedades, operadores… que se usen, será un tipo válido. P. ej. **C++ usa tipado estructural en los templates **(el equivalente a genéricos):

template <typename T, typename U> T Add (T a, U b) {
   return a+b;
}

Esta función Add se puede invocar con cualquier par de tipos T y U para los cuales haya definido un operator+. El tipado estructural hace innecesarios los type classes a costa de trasladar la responsabilidad al llamante. Es decir, si en C++ intentas llamar a esta función con un par de tipos (T,U) para los cuales no haya el operator+ definido, el código no compilará. Insisto: no compilará. Tipado estructural no es duck typing, los errores los sigues teniendo en tiempo de compilación.

Observa que el compilador no puede asumir nada sobre los tipos T y U en el contexto de la función Add. Eso significa que no te puede ayudar si te equivocas en un nombre de un método de T o U y es imposible que herramientas como intellisense puedan aplicarse.

Por lo tanto, el tipado estructural es bueno, pero viene con el precio de que el compilador no te puede ayudar mucho dentro de la función template y que no hay una manera que puedas especificar qué requiere una función. La gente de C++ está al tanto de esa limitación del tipado estructural y por ello en C++20 se incorpora lo que se conoce como “conceptos“. Los conceptos permiten especificar restricciones de parámetros templates (equivalentes a las restricciones que podremos especificar con los type classes) a la vez que se mantiene el tipado estructural.

En resúmen

Si esa característica se concreta me parece uno avance importantísimo al lenguaje. Los genéricos tal y como están ahora mismo sufren de grandes limitaciones que con los type classes se podrán solucionar en gran medida. Por supuesto quedan varios flecos por definir y habrá que estar atentos a como evoluciona todo, pero de momento, solo el hecho de que estén pensando en ello ya es buena señal 🙂

Si quieres, puedes invitarme a un café xD

eiximenis
ESCRITO POR
eiximenis
Compulsive Developer