Tipos Genéricos en Swift

Autor: | Última modificación: 15 de noviembre de 2022 | Tiempo de Lectura: 5 minutos
Temas en este post:

Tipos Genéricos en Swift o Incompletos

En el artículo anterior, habíamos hablado de los tipos y protocolos completos en Swift.

En este artículo seguiremos explorando los tipos avanzados en Swift. Sin embargo, lo primero es hacer repaso de los visto en la anterior entrega.

Los tipos y protocolos completos son aquellos que proporcionan toda la información necesaria al compilador:

  • el tipo completo proporciona toda la información necesaria y otra más que no hace falta en el contexto actual.
  • el protocolo completo es aquel que proporciona toda la información necesaria, pero sólo aquella necesaria.

Por el nombre, es bastante evidente que los tipos incompletos son aquellos que no especifican toda la información que requiere el compilador. Como tal vez ya hayas adivinado, tipo incompleto, no es más que otro nombre —que me he inventado yo— para tipo genérico.

Más que un tipo, es una plantilla para tipos. ¿Un ejemplo? ¡Faltaría más!

Un buen ejemplo son las colecciones, como Array o Dictionary. No tendría sentido hacer un Array para Ints, otro para String, otro para…. En vez de eso, es preferible crear una plantilla de Array que funciona con cualquier elemento. Aquí es donde entran los tipos genéricos que posiblemente ya conocemos de otros lenguajes como Java o C++.

Veamos un ejemplo muy común en C++, el tipo Pair:

struct Pair <First, Second>
  let first  : First
  let second : Second
}

First y Second son plantillas que tendrán que recibir su valor real en tiempo de compilación para poder crear un tipo específico de Pair.

Reificación (toma ya)

let entry : Pair<String, String> // sin problemas
let nope  : Pair // ¡Error al canto!

La primera línea compila sin problemas y no genera un error. Sin embargo, la segunda causaría un error de compilación. ¿Por qué?

En la primera linea hemos creado un tipo completo a partir de uno incompleto: hemos rellenado la plantilla antes de seguir. Con esto, el compilador de Swift tiene toda la información necesaria para crear una pareja de dos cadenas.

La segunda linea, sin embargo, no proporciona la información extra necesaria. Al compilador le falta saber exactamente quién puñetas es First y Second. Sin eso no puede seguir y no compila.

Por lo tanto, para poder usar un tipo incompleto (lo que Swift llama un genérico) hay que aportarle la información que le falta, creando así un tipo completo.

A este proceso, se le llama reificación (¡chúpate esa!).  La tal reificación, es el proceso de pillar a alguien que no está del todo especificado,  y bajarlo de las nubes hasta que todo esté especificado, sellado, firmado, oleado y sacramentado y no quede ninguna duda posible (tal y como le gusta al compilador de Swift).

Restricciones Genéricas

Hemos visto que los tipos genéricos (lo que nosotros habíamos llamado tipos incompletos) tienen «huecos» en los que podemos meter otros tipos. Por ejemplo, en el tipo Pair, tenemos dos huecos a los que de forma muy original he llamado first y second.

Hasta ahora, podemos meter cualquier cosa ahi dentro: dos String, un UIViewController y un Int, o cualquier otra combinación que se me ocurra.

Ahora bien, a veces puede interesar restringir lo que se puede meter en esos agujeros. Como una cerradura que sólo acepta un cierto tipo de llave.

Esas restricciones están un poco… restringidas. 😉 Es decir, no vale cualquier cosa.

Las restricciones sólo pueden ser sobre el cumplimiento de un protocolo.

Es decir, en Swift podemos expresar cosas como:

una Pair en la cual el first implementa el procotolo Hashable.

Sin embargo, no podemos expresar (todavía) cosas como:

una Pair de dos Int, y el primero ha de ser siempre mayor que el segundo.

Para el segundo tipo necesitaríamos lo que se llama tipos dependientes, y aun no están soportados en Swift, pero puede que lo estén en el futuro.  En otro artículo veremos cómo podemos simularlo en Swift.

struct Cons< Head : Hashable, Tail : Hashable > {}

Cons es una especie de Pair, pero cuyos elementos tienen que cumplir con una condición: implementar el protocolo Hashable.

Más de una restricción

¿Qué pasa si alguien tiene que cumplir con más de una restricción? Para eso tenemos la palabra clave where. Supongamos que necesitamos que el Head sea Hashable y también Equatable. Chupao:

struct Cons <Head, Tail>
  where Head : Hashable,
        Head : Equatable,
        Tail : Hashable {

  // Head es Hashable y (AND) Equatable: las dos cosas a la vez
}

¡OJO! Podemos decir que Head es Hashable y Equatable. Es decir, los dos protocolos se combinan mediante un And. Sin embargo, no podemos decir que Head ha de ser Hashable o Equatable.

Si no entiendes el por qué, piensa qué tendría que hacer el compilador en el segundo caso: en el fondo no sabría qué puñetas es un Head y no podría compilar. Para estos casos, están las Enum, que expresan tipos que pueden ser una cosa u otra (por ejemplo, un Optional puede estar lleno o vacío).

Extensiones sobre tipos restringidos

Una novedad interesante en Swift 3.0 es que podemos añadir funcionalidades a tipos genéricos pre-existentes, dependiendo de la restricción que tenga.

Esto se entiende mucho mejor con un ejemplo, así que vamos allá.

Supongamos que tenemos el tipo genérico Pair, sin ninguna forma de restricción:

struct Pair<First, Second>{
 first  : First
 second : Second
}

Es decir, first y second pueden ser lo que les dé la gana.

A mi me gustaría que Pair tuviese un método que me devuelva un diccionario con first como la clave y second como el valor.

Esto sólo tiene sentido si first implementa Hashable (es una condición que exige Dictionary a cualquier tipo que quiera ser una clave).

Uséase, yo quiero representar lo siguiente:

Pair está compuesto por un first y un second que pueden ser lo que les dé la gana. Ahora bien, si first es Hashable, quiero que Pair tenga el método asDictionary().

En Swift, lo de arriba se diría así:

struct Pair<First, Second>{
 first  : First
 second : Second
}

extension Pair 
  where First : Hashable{

  func asDictionary() -> [First : Second]{
    return [first : second]
  }
}

Cualquier otro tipo de Pair no tendrá ese método.

Limitaciones de la sintaxis genérica

Hemos visto que los tipos genéricos nos permiten representar muchísimas situaciones y que además el compilador se encarga de comprobarlo todo para nosotros.

Parece que tenemos una herramienta estupenda en nuestras manos. Sin embargo, nuestra herramienta mágica tiene una limitación muy seria: su sintaxis.

El invento de indicar los «huecos» de la plantilla como nombres que empiezan por mayúsculas y entre corchetes angulares (<>) tiene un problema grave: no escala.

A lo que me refiero, es que a medida que tienes plantillas (tipos genéricos) más complejas y plantillas dentro de plantillas, la sintaxis se vuelve inmanejable e imposible de entender.

Hay una forma de mantener todas las ventajas de los genéricos y evitar la sintaxis endemoniada. Esa forma, es la que se ha usado en los protocolos genéricos.

Protocol can only be used as a generic constraint because it has Self or associated type requirements.

Eso lo veremos en un próximo artículo, donde además terminaremos de entender el error extrafalario que nos ha traído hasta aquí, cómo resolverlo y qué ventajas tiene la programación orientada a protocolos si la comparamos a la programación orientada a objetos. ¡Casi nada!

Si tienes algo que deseas compartir o quieres formar parte de KeepCoding, escríbenos a [email protected].