some: el tipo que el compilador conoce y tú no
Un tipo opaco esconde el tipo concreto al lector pero no al compilador. Genéricos invertidos, identidad de tipo preservada, despacho estático y coste cero. Por qué todas las ramas deben devolver el mismo tipo, qué significa some en posición de parámetro, y en qué se diferencia de una simple abreviatura.
some View parece una forma vaga de decir “algo que es una vista”, y esa lectura, aunque cómoda, es falsa. No hay nada vago en un tipo opaco: el compilador conoce el tipo concreto con total precisión, lo usa para especializar el código, para verificar la igualdad y para eliminar toda indirección. Lo único que ocurre es que el tipo no aparece escrito en la firma. some no es imprecisión: es ocultación deliberada de información que sí existe, un cortafuegos entre la implementación y quien la usa, con precio cero en tiempo de ejecución.
- Leer
some Pcomo un genérico invertido, donde el tipo lo elige la implementación y no quien llama. - Comprender la identidad de tipo: dos llamadas a la misma función devuelven exactamente el mismo tipo.
- Justificar por qué todas las ramas de retorno deben coincidir y cómo lo sortea un constructor de resultados.
- Distinguir
someen posición de parámetro como azúcar sintáctico sobre un parámetro genérico.
Un tipo concreto con la cara tapada
Un tipo opaco declara un contrato de salida: devuelvo un tipo, siempre el mismo, que conforma a este protocolo, y no te digo cuál.
protocol Figura {
var area: Double { get }
}
struct Rectangulo: Figura {
let ancho: Double, alto: Double
var area: Double { ancho * alto }
}
func figuraUnidad() -> some Figura { // el compilador sabe: Rectangulo
Rectangulo(ancho: 1, alto: 1) // quien llama solo sabe: es una Figura
}
Compáralo con la forma genérica clásica y verás que la dirección de la elección se ha invertido:
// Generico normal: quien LLAMA elige T
func vacia<T: Figura>() -> T { ... } // imposible de implementar en general
// Tipo opaco: quien IMPLEMENTA elige el tipo
func figuraUnidad() -> some Figura { ... } // natural y verificable
Por eso a los tipos opacos se les llama genéricos invertidos o genéricos inversos. En un genérico normal, el parámetro de tipo es una entrada que fija el punto de llamada; en un opaco, es una salida que fija el cuerpo de la función. La firma es idéntica en apariencia y opuesta en semántica.
Identidad de tipo: la propiedad que lo cambia todo
Aquí está la diferencia decisiva frente a una caja existencial. Un tipo opaco preserva la identidad del tipo: el compilador sabe que dos invocaciones de la misma función producen valores del mismo tipo, aunque tú no puedas nombrarlo.
func par() -> some Equatable { 42 }
let a = par()
let b = par()
print(a == b) // compila: el compilador sabe que ambos son Int
Ese == es imposible sobre un existencial, porque Equatable tiene un requisito de Self y dos cajas podrían guardar tipos distintos. Con some, la promesa “siempre el mismo tipo” hace que el requisito de Self quede satisfecho estáticamente. La identidad no es un detalle: es lo que permite que un tipo opaco conforme al protocolo, se use en genéricos, satisfaga requisitos con tipos asociados y participe en operaciones binarias.
La contrapartida es la restricción que todo el mundo descubre a base de errores: todas las rutas de retorno deben producir el mismo tipo concreto.
func figura(grande: Bool) -> some Figura {
if grande { return Rectangulo(ancho: 10, alto: 10) }
else { return Circulo(radio: 3) } // error: tipos distintos
}
No es una limitación arbitraria, es la consecuencia directa de la promesa. Si dos llamadas pudieran devolver tipos diferentes, la identidad se rompería y == dejaría de tener sentido. La solución idiomática cuando de verdad necesitas ramificar es envolver ambas ramas en un tipo genérico único —el patrón que emplea internamente el constructor de vistas de SwiftUI, que empaqueta el if en un tipo condicional parametrizado por las dos alternativas—.
Cuando escribes un if dentro de un body de SwiftUI y compila, no has violado la regla: el atributo constructor de resultados ha reescrito tu código para que ambas ramas se envuelvan en un único tipo genérico condicional. El tipo de retorno sigue siendo uno solo, solo que ahora es un tipo que contiene la disyuntiva en sus parámetros. La regla no se rompe; se satisface a otro nivel.
some en parámetros: genéricos con otra sintaxis
Desde Swift 5.7, some también puede aparecer en la posición de un parámetro. Ahí su significado es exactamente el de un parámetro genérico anónimo.
func imprimirArea(de figura: some Figura) { print(figura.area) }
// Es estrictamente equivalente a:
func imprimirArea<T: Figura>(de figura: T) { print(figura.area) }
Fíjate en la simetría y en la asimetría. En entrada, some significa “quien llama elige el tipo”. En salida, significa “yo elijo el tipo”. Es la misma palabra porque en ambos casos describe un tipo concreto y único, oculto tras el protocolo; cambia quién ejerce la elección, y eso lo determina la posición.
También puedes acotar los tipos asociados sin abandonar la opacidad, gracias a los tipos asociados primarios:
func enteros() -> some Collection<Int> { // opaco, pero con Element fijado
[1, 2, 3]
}
func sumar(_ xs: some Sequence<Int>) -> Int {
xs.reduce(0, +)
}
flowchart LR L[Quien llama] -->|elige el tipo| PE[some en parametro] I[Quien implementa] -->|elige el tipo| PS[some en retorno] PE --> U[Un unico tipo concreto en cada uso] PS --> U U --> C[Identidad de tipo preservada] C --> D[Despacho estatico y posible especializacion] D --> Z[Sin caja y sin asignacion de memoria] style U fill:#a6e3a1,color:#11111b style C fill:#89b4fa,color:#11111b style Z fill:#cba6f7,color:#11111b
Coste cero y sus letras pequeñas
Un tipo opaco no introduce ninguna estructura en ejecución. No hay caja, no hay asignación de memoria, no hay salto indirecto obligatorio: el código generado es el mismo que si hubieras escrito el tipo concreto a mano. La opacidad vive enteramente en el comprobador de tipos.
func contador() -> some Collection<Int> { Array(1...100) }
let c = contador()
let total = c.reduce(0, +) // se compila igual que si c fuese [Int]
El opaco también se propaga hacia dentro de estructuras compuestas. Desde Swift 5.7 puedes colocarlo en posiciones estructurales del tipo de retorno, no solo en la raíz, lo que permite devolver tuplas u opcionales de tipos ocultos sin renunciar a la identidad de cada uno:
func partes() -> (some Collection<Int>, some Figura) {
([1, 2, 3], Rectangulo(ancho: 1, alto: 1))
}
func quiza() -> (some Figura)? {
Rectangulo(ancho: 2, alto: 2)
}
La letra pequeña, que casi nunca se cuenta, es que coste cero describe el modelo, no siempre el binario. El código genérico sin especializar también usa tablas en ejecución; lo que garantiza some es que el optimizador puede especializar, porque conoce el tipo. Dentro de un módulo, con optimización de módulo completo, la especialización es la norma. A través de fronteras de módulo, sin @inlinable ni resiliencia relajada, el compilador puede verse obligado a emitir la versión genérica no especializada. La diferencia con any sigue siendo cualitativa —no hay caja ni borrado—, pero conviene no confundir el modelo con la garantía absoluta.
some Figura oculta el tipo a la comprobación de tipos, no a la introspección en ejecución. El valor sigue llevando sus metadatos, y una comprobación dinámica de tipo puede revelar que era un Rectangulo. La opacidad es una disciplina de diseño de interfaz —te permite cambiar el tipo interno sin romper a quien te usa—, no un mecanismo de seguridad ni de ocultación de datos.
Lo que hace profundo a some no es lo que oculta, sino lo que te permite dejar de prometer. Cuando declaras un retorno concreto, el tipo entra en tu contrato público: cambiar de un array a un conjunto rompe a quien dependía del índice entero. Cuando declaras some Collection, tu compromiso se reduce a las capacidades que enumera el protocolo, y el tipo concreto pasa a ser un detalle libre, revisable en la siguiente versión sin romper a nadie. Es exactamente la diferencia entre acoplar por implementación y acoplar por capacidad, pero expresada dentro del sistema de tipos y verificada por el compilador, no confiada a la disciplina de quien programa. Y aquí está la elegancia: normalmente ese desacoplamiento se paga con indirección —una interfaz, una tabla virtual, una caja—, porque ocultar suele implicar no saber. some rompe esa equivalencia: el compilador sí sabe, y por tanto puede optimizar como si nada estuviera oculto, mientras que el sistema de tipos no deja saber, y por tanto tu API queda libre. Es abstracción sin indirección, dos cosas que la ingeniería de software lleva décadas dando por inseparables. Esa es la razón última de que toda vista de SwiftUI tenga un cuerpo opaco: los tipos reales de una jerarquía de vistas son monstruos anidados de decenas de parámetros que ningún ser humano querría escribir, y sin embargo el compilador necesita conocerlos íntegros para generar código plano, comparar estructuras y decidir qué redibujar. some es el único mecanismo que permite ambas cosas a la vez: un tipo enorme y exacto para la máquina, una firma de tres palabras para la persona.
- Escribe una función que devuelva
some Sequence<Int>con un rango en su interior; intenta luego indexarla con un entero y explica qué capacidad te falta. - Declara dos funciones distintas que devuelvan
some Equatablecon el mismo tipo interno e intenta comparar sus resultados entre sí; razona el error a partir de la identidad de tipo. - Convierte una función genérica tuya a la forma de parámetro
somey comprueba que el cuerpo no necesita ningún cambio. - Provoca a propósito el error de ramas con tipos distintos y después resuélvelo envolviendo ambas alternativas en un mismo tipo genérico definido por ti.
- Cambia el tipo interno devuelto por una función opaca de array a conjunto y observa qué código cliente sigue compilando y cuál no: acabas de medir tu superficie de compromiso.