Cláusulas where: restricciones sobre tipos asociados
La cláusula where y las exigencias sobre tipos asociados: restricciones anidadas, requisitos de mismo tipo y where en extensiones. Aquí vive el poder real del sistema genérico de Swift.
La lista de parámetros solo sabe hablar del parámetro. En cuanto necesitas exigir algo sobre lo que ese parámetro contiene —el elemento de una colección, el índice de una secuencia, el error que lanza— hay que bajar a la cláusula where. Es la pieza que convierte a los genéricos de Swift en un sistema expresivo de verdad, y la que hace posible que la librería estándar sea como es.
- Expresar restricciones sobre tipos asociados con la cláusula
where. - Usar requisitos de mismo tipo para forzar que dos tipos asociados coincidan.
- Aplicar
whereen extensiones para añadir API solo a ciertas instanciaciones. - Reconocer cuándo la sintaxis de dos puntos basta y cuándo
wherees imprescindible.
El límite de los dos puntos
Un protocolo con tipos asociados describe una familia, no un tipo. Collection no dice qué guarda: declara un tipo asociado Element que cada conformidad concreta rellena. Restringir el parámetro no restringe su contenido.
func sumar<C: Collection>(_ c: C) -> Int {
// c.reduce(0, +) // error: el elemento podria ser cualquier cosa
c.count
}
La cláusula where se coloca al final de la firma, justo antes de la llave de apertura, y ahí sí puedes nombrar tipos asociados con notación de punto:
func sumar<C: Collection>(_ c: C) -> Int where C.Element == Int {
c.reduce(0, +)
}
func ordenado<C: Collection>(_ c: C) -> [C.Element] where C.Element: Comparable {
c.sorted()
}
Las dos formas son distintas y no intercambiables. C.Element == Int es un requisito de mismo tipo: exige identidad exacta. C.Element: Comparable es un requisito de conformidad: exige que el elemento, sea el que sea, cumpla un protocolo. La primera cierra la puerta a todo lo que no sea ese tipo; la segunda la deja abierta a una familia entera.
func f<T: Equatable>(_ x: T) y func f<T>(_ x: T) where T: Equatable generan exactamente la misma firma. La versión con dos puntos es azúcar. Cuando una función tiene tres o más restricciones, moverlas todas a where mejora la legibilidad: los parámetros quedan limpios y los contratos alineados en una sola zona.
Requisitos de mismo tipo y encadenamiento
El requisito de mismo tipo es la herramienta que permite relacionar dos parámetros genéricos entre sí. Sin él, dos colecciones distintas no tienen ninguna conexión, aunque guarden lo mismo.
func intercalar<A: Sequence, B: Sequence>(
_ a: A, _ b: B
) -> [A.Element] where A.Element == B.Element {
var salida: [A.Element] = []
var ia = a.makeIterator(), ib = b.makeIterator()
while let x = ia.next(), let y = ib.next() {
salida.append(x); salida.append(y)
}
return salida
}
Las restricciones se encadenan por profundidad arbitraria: puedes hablar del tipo asociado del tipo asociado. Esto es lo que permite firmas como las de los algoritmos de la librería estándar, que navegan varios niveles de la jerarquía de protocolos.
func aplanarOrdenado<C: Collection>(_ c: C) -> [C.Element.Element]
where C.Element: Collection, C.Element.Element: Comparable {
c.flatMap { $0 }.sorted()
}
flowchart TD A[Parametro C] --> B[Contrato: C es Collection] B --> C[Tipo asociado C.Element] C --> D[Requisito de conformidad: Element es Comparable] C --> E[Requisito de mismo tipo: Element igual a Int] D --> F[Habilita sorted y max] E --> G[Habilita suma aritmetica] style D fill:#a6e3a1,color:#11111b style E fill:#f9e2af,color:#11111b
where en extensiones: API que aparece sola
El uso más elegante de where no está en funciones sueltas sino en extensiones. Una extensión con cláusula where añade miembros que solo existen cuando la instanciación concreta cumple la condición. El autocompletado los muestra y los oculta según el tipo real.
extension Array where Element: Numeric {
var total: Element { reduce(.zero, +) }
}
extension Collection where Element: Equatable {
func todosIguales() -> Bool {
guard let primero = first else { return true }
return allSatisfy { $0 == primero }
}
}
[1, 2, 3].total // 6
["a", "b"].total // error en compilacion: String no es Numeric
Este mecanismo es el que explica por qué sorted() aparece en un array de enteros y no en uno de closures, o por qué joined() solo existe cuando el elemento es a su vez una secuencia. No hay magia ni casos especiales del compilador: es la librería estándar usando la misma cláusula where que tienes tú.
Sobre el parámetro
where T: Equatable. Idéntico a la forma con dos puntos. Útil para descargar la lista de parámetros cuando hay muchas restricciones.
Sobre un tipo asociado
where C.Element: Comparable. Imposible de expresar con dos puntos. Es la razón de ser de la cláusula.
De mismo tipo
where A.Element == B.Element. Ata dos parámetros independientes obligándolos a coincidir en un punto concreto.
También puedes restringir sobre Self dentro de una extensión de protocolo, lo que permite dar implementaciones por defecto condicionales sin tocar la declaración del protocolo:
protocol Repositorio {
associatedtype Modelo
func todos() -> [Modelo]
}
extension Repositorio where Modelo: Identifiable {
func buscar(_ id: Modelo.ID) -> Modelo? {
todos().first { $0.id == id }
}
}
Dos extensiones con la misma función y cláusulas where distintas no son ambiguas: Swift elige la más específica. Pero si ninguna es estrictamente más específica que la otra, el error de ambigüedad aparece en el punto de llamada y suele ser críptico. Diseña las restricciones de modo que formen un orden claro.
Detente en lo que acabas de ver, porque es la razón por la que el sistema genérico de Swift se estudia y no solo se usa. La cláusula where no añade sintaxis: añade cuantificación sobre la estructura interna de los tipos. Un requisito de conformidad dice “para todo tipo que cumpla esto”; un requisito de mismo tipo dice “y además estos dos caminos por el grafo de tipos asociados llegan al mismo sitio”. Con esas dos primitivas, el compilador construye un sistema de reescritura que decide si un conjunto de restricciones es satisfacible, si es redundante o si es contradictorio, y lo hace antes de generar código. Eso es lo que permite que Array, Set, String, Range y tus propios tipos compartan literalmente los mismos algoritmos, escritos una sola vez, sin una línea de despacho dinámico y sin una sola conversión implícita. La librería estándar de Swift no es una colección de tipos con métodos: es un conjunto de teoremas sobre protocolos, y las cláusulas where son sus hipótesis. Cuando llegues al punto en que diseñas tus propias jerarquías con tipos asociados y las restringes con precisión, dejas de escribir Swift y empiezas a extender su gramática.
- Escribe una extensión de
Collectioncon una propiedadpromedioque solo exista cuando el elemento seaBinaryFloatingPoint. - Declara
func zipIgualesque acepte dos secuencias con requisito de mismo tipo y devuelva los pares en las posiciones donde coinciden. - Convierte una función tuya con tres restricciones de dos puntos a la forma con
wherey compara la legibilidad. - Diseña un protocolo con dos tipos asociados y una extensión que los relacione con un requisito de mismo tipo. Provoca a propósito una restricción contradictoria y lee el diagnóstico.