Curiosidades de C#: tipado estructural… sólo para algunos

Ya vimos hace tiempo la posibilidad de simular duck typing en C#, pero lo que seguro que alguno no sabe es que C# tiene algo más potente que el duck typing: el tipado estructural.

El tipado estructural lo mencionamos al hablar de los sistemas de tipos de otros lenguajes y es similar al duck typing en cuanto a que no hace falta declarar a priori que estamos implementando un interfaz concreto, pero además se verifica en tiempo de compilación.

Es decir, si un método necesita recibir un objeto que tenga un método void Add(int a, int b), y le pasamos un objeto que tiene ese método, aunque no hayamos indicado explícitamente que implementa un interface IAdder con ese método, nuestro código funcionará.

¿Suena bien? La verdad es que es una características muy interesante que podéis encontrar en lenguajes como scala, y aunque desgraciadamente en C# no es una características que tengamos accesible a (todo) nuestro código, el compilador sí que es capaz de hacerlo en algunos casos. No es que le podamos sacar mucho partido, pero al menos es curioso conocer estos casos.

foreach

El caso más sencillo es el de foreach. Todo el mundo sabe como funciona un foreach: nos permite recorrer cualquier objeto que implemente el interface IEnumerable o IEnumerable<T> y procesarlo elemento a elemento.

Bueno, no solo eso, porque de la época de .NET 1.0, cuando nos vestíamos con pieles de mamut y no teníamos tipos genéricos, también nos permite hacer un cast del elemento según vamos recorriendo el enumerable para simplificar un poco el código.

Lo curioso es que aquí el compilador de C# permite utilizar tipado estructural, y en realidad no es necesario que el objeto que recorremos con foreach implemente IEnumerable, basta con que el objeto tenga un método IEnumerator GetEnumerator().

Podéis probarlo con el siguiente ejemplo:

public class ImNotAnEnumerable {
  public IEnumerator GetEnumerator() {
    yield return "I'm";
	yield return "not";
	yield return "an";
	yield return "enumerable!";
  }
}

var notAnEnumerable = new ImNotAnEnumerable();
foreach (var value in notAnEnumerable) {
  Console.WriteLine(value);
}

¿Vale para algo? Seguro que existe algún motivo para esta peculiaridad del compilador de C# (y si alguien lo conoce, seguro que es Eric Lipper) pero a simple vista no parece que sea algo que podamos aprovechar mucho en nuestro código. Teniendo en cuenta que el único que puede sacar partido de esto es el foreach y que implementar IEnumerable cuando ya has implementado GetEnumerator es trivial, no se me ocurre para qué podría servir.

LINQ: from…select

El caso del foreach es bastante conocido, pero existe también otro caso parecido con la sintaxis de consultas de LINQ:

var result = from x in SomeQuery()
		     from y in OtherQuery(x)
			 select y.ToString();

Todo el mundo sabe (o debería saber) que esa sintaxis es equivalente a invocar unos cuantos extension methods sobre IEnumerable:

var result = SomeQuery()
               .SelectMany(x => OtherQuery(x))
			   .Select(y => y.ToString();

Cabría esperar que esta sintaxis sólo funcionase sobre IEnumerable, pero viendo el tema del post, ya os imagináis que no es así. Para que funcione esta sintaxis, sólo hace falta definir extenion methods para Select y SelectMany sobre la clase que queramos usar.

Vamos a ver un ejemplo (que ya os adelanto que no tiene mucho sentido) para entenderlo mejor. Suponed que tenemos una clase Box que encapsula un valor (aquí es donde los más espabilaos empezáis a pensar en monads):

class Box<A>
{
  private readonly A value;

  public Box(A value) {
    this.value = value;
  }

  public A Value {
    get { return value; }
  }
}

Y ahora supongamos (y esto ya es mucho suponer) que queremos poder escribir este código para operar con los valores encapsulados en objetos Box:

var b1 = new Box<int>(37);
var b2 = new Box<string>("El valor es {0}");

var msg = from n in b1
          from format in b2
		  select string.Format(format, n);

Console.WriteLine(msg.Value);
// >> "El valor es 37

Para que ese código compilase, podríamos hacer que Box implementase IEnumerable y su enumerador devolviese una colección con el valor encapsulado, pero eso haría que al utilizar la sintaxis de LINQ dejásemos de tener objetos Box y tuviéramos IEnumerables, por lo que no podríamos hacer msg.Value (habría que usar msg.First() o algo parecido).

Si aprovechamos el tipado estructural del que llevamos todo el rato hablando, podemos crear la siguiente clase con un par de extension methods análogos a los que existen para IEnumerable<T>, pero operando con objetos Box:

public static class BoxExtensions
{
  public static Box<B> Select<A, B>(this Box<A> box, Func<A, B> projection) {
    return SelectMany(box, _ => box, (a, b) => projection(a));
  }

  public static Box<C> SelectMany<A, B, C>(this Box<A> box, Func<A, Box<B>> function, Func<A, B, C> projection) {
    return new Box<C>(projection(box.Value, function(box.Value).Value));
  }
}

De acuerdo, es un poco feo, pero básicamente lo único que hace es implementar SelectMany como la aplicación de una proyección a los valores encapsulados en dos objetos Box y volver a empaquetar el resultado en un objeto Box. Si eres de los que antes empezó a pensar en monads, esto te sonará a bind, y si vas por esa vía la implementación queda bastante más limpia.

Con esos extension methods, y sin haber implementado ningún interface en la clase Box, el compilador ya se queda contento y podemos ejecutar el código que mostrábamos antes. Esto no sólo es aplicable a Select y SelectMany, también es válido para métodos como Join.

Al igual que en el caso del foreach, la pregunta está clara: ¿vale esto para algo? Sinceramente no lo he utilizado jamás, entre otras cosas porque no me gusta nada la sintaxis de LINQ y prefiero usar invocaciones de métodos normales. Aun así, creo que hay escenarios en los que puede dar lugar a un código bastante legible con una sintaxis similar al do notacion de Haskell o F#.

Si tuviésemos algo parecido a la maybe monad que nos explicaba Eduard Tomàs hace tiempo, y estuviésemos parseando un fichero, podríamos escribir código como éste:

Maybe<string> ReadName() {...}
Maybe<int> ReadAge() {...}

Maybe<Person> ReadPerson() {
  return from name in ReadName()
         from age in ReadAge()
         select new Person(name, age);
}

El tema de la legibilidad es cuestión de gustos y, sobre todo, de costumbres, pero este formato habrá a quién le guste.

En resumen…

Todo este post no deja de ser una excusa para ver un par de curiosidades del compilador de C# y hacer guarrerías con el código, que es lo que más nos gusta.

La utilidad real de todo esto es, como hemos visto, más que cuestionable, pero siempre es divertido conocer mejor las cosas que utilizamos todos los días.

Seguro que hay más casos como estos en los que el compilador de C# hace trampas y utiliza tipado estructural para dar soporte a algunas construcciones sintácticas, pero yo no los conozco (¿async/await tal vez?).

¿Conocéis alguno más?


10 comentarios en “Curiosidades de C#: tipado estructural… sólo para algunos

  1. El caso del foreach es sencillo, y no es nada “del compilador”… está explicito en la especificación de C# (punto 8.8.4) y cualquier compilador completo de C# debe implementarlo igual (si algo bueno -IMHO- tiene C# es que nada está hecho al azar, está todo especificado):

    “The compile-time processing of a foreach statement first determines the collection type, enumerator type and element type of the expression. This determination proceeds as follows:
    // etc. //
    • Otherwise, determine whether the type X has an appropriate GetEnumerator method:
    // etc. //

  2. Visual Studio instala la especificación de C# por defecto, si la tienes en la ruta normal, la tienes en (para VS 2015) C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC#\Specifications\1033.

    Lo de las expresiones viene en 7.16.2. (las negritas son mías):

    “The C# language does not specify the execution semantics of query expressions. Rather, query expressions are translated into invocations of methods that adhere to the query expression pattern (§7.16.3). Specifically, query expressions are translated into invocations of methods named Where, Select, SelectMany, Join, GroupJoin, OrderBy, OrderByDescending, ThenBy, ThenByDescending, GroupBy, and Cast.These methods are expected to have particular signatures and result types, as described in §7.16.3. These methods can be instance methods of the object being queried or extension methods that are external to the object, and they implement the actual execution of the query.

    The translation from query expressions to method invocations is a syntactic mapping that occurs before any type binding or overload resolution has been performed

  3. Lla especificación es compleja para entender de un vistazo, pero “creo” que viene a decir que lo único que hace es convertir las expresiones de linq a llamadas a métodos, antes siquiera de hacer ninguna resolución de tipos.

    Supongo que es más optimizado que estar buscando la extensión de método propiamente dicha

    No me la he leido del todo ni le he prestado atención, así que puedo estar equivocado

  4. Ah, y me faltarían negritas en “These methods can be instance methods of the object being queried or extension methods that are external to the object”, que es lo realmente importante :-)

  5. Muchas gracias por la (detallada) aclaración.

    No sabía que las specs se instalaban con el Visual Studio, a partir de ahora me las leeré detenidamente cada vez que instale una versión nueva :-P

  6. Yo no sabía que se instalaban las specs con VS y me acabo de dar cuenta que si instalas la versión en español del VS el documento de la especificación esta en español también (por lo menos en VS 2015)

  7. A mi me pondría nervioso usar VS en español :-/ La especificación en español debe ser cuando menos graciosa (viendo las traducciones no-automáticas de la MSDN, hay cosas que chirrían).

    El código también suelo hacerlo (salvo para cosas muy concretas o si he tenido que trabajar con más gente) totalmente en inglés, salvo las cadenas literales… y sólo si esas cadenas son para el usuario (en cuyo caso generalmente las pongo en recursos), en caso contrario, también en inglés :-)

  8. Actualmente trabajo en dos oficinas diferentes, en una tengo el VS en español y en la otra en ingles, y ningún problema con la versión en español del VS … a estas alturas creo que MS tendría que tener resuelto el tema de la traducción y a nosotros solo nos tiene que quedar elegir con lo que seamos más productivos.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos necesarios están marcados *

*

Puedes usar las siguientes etiquetas y atributos HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>