El lenguaje que viene: ownership, macros, embedded y concurrencia
Swift no evoluciona al azar: cuatro vectores tiran en la misma dirección desde hace años. Memoria sin copias con vidas verificadas, metaprogramación de primera clase, un perfil sin runtime para sistemas embebidos y una concurrencia que por fin se hace ergonómica. Qué viene, por qué, y qué conviene preparar hoy.
Predecir el futuro de un lenguaje es fácil cuando el lenguaje publica su plan. Swift lo hace: cada cambio pasa por una propuesta escrita, un periodo de revisión pública y una decisión razonada, y las propuestas en vuelo se leen hoy. Pero leer la lista no basta, porque una lista de cuarenta propuestas parece ruido. Lo que hay debajo son cuatro vectores que llevan años empujando de forma coordinada, y un objetivo común: que Swift pueda ocupar, con el mismo modelo de seguridad, el rango completo que va del firmware de un microcontrolador a un servidor con miles de tareas concurrentes, sin pedirle al programador que aprenda un lenguaje distinto en cada extremo. Esta lección explica los cuatro vectores, el mecanismo que permite cambiar un lenguaje con ABI estable sin romper nada, y qué merece la pena que hagas hoy para no ir a remolque.
- Explicar el vector de ownership: tipos no copiables, no escapables y vistas de memoria prestada.
- Situar las macros y la metaprogramación en la estrategia general y reconocer su coste real.
- Describir qué es Embedded Swift, qué renuncia y qué abre.
- Entender los modos de lenguaje y las banderas de features como el mecanismo que hace posible evolucionar sin romper.
Ownership: hacia el cero copias verificado
El vector más profundo empezó con borrowing y consuming, siguió con los tipos no copiables marcados ~Copyable y su extensión a los genéricos, y ahora avanza hacia el problema que faltaba: cómo representar una vista sobre memoria ajena sin que esa vista pueda sobrevivir a lo que mira. La respuesta es la combinación de dos ideas. Los tipos no escapables, escritos ~Escapable, no pueden guardarse en propiedades, capturarse en cierres ni devolverse sin más; y las dependencias de vida útil, que atan explícitamente la vida de un valor devuelto a la de un argumento.
De ahí sale Span, la vista segura sobre memoria contigua que sustituye a UnsafeBufferPointer en la inmensa mayoría de los casos.
func suma(_ v: Span<Double>) -> Double {
var t = 0.0
for i in v.indices { t += v[i] }
return t
}
let numeros = [1.0, 2.0, 3.0]
let total = suma(numeros.span) // sin copia, sin puntero crudo, sin ambito anidado
La diferencia con el pasado es enorme y conviene medirla. Antes, acceder a la memoria contigua de un array sin copiarla exigía withUnsafeBufferPointer y un cierre anidado, y la seguridad dependía de que tú no dejaras escapar el puntero. Ahora el compilador comprueba que la vista no sobreviva al array, el código se lee en línea recta y el coste sigue siendo cero.
// Antes: el ambito anidado era la unica forma de acotar la vida del puntero
let total = numeros.withUnsafeBufferPointer { buf -> Double in
var t = 0.0
for x in buf { t += x }
return t // si devolvieras buf, compilaria y seria un desastre
}
Ese último comentario es el punto entero del vector: la versión con puntero permite escribir el error y confía en tu vigilancia; la versión con Span no lo permite, porque la dependencia de vida útil convierte «esta vista sobrevive a su origen» en un diagnóstico del compilador. En paralelo llega InlineArray, un array de tamaño fijo almacenado en línea sin asignación en el montículo, que cierra el hueco entre las tuplas y Array para el código de sistemas.
struct Paquete: ~Copyable { // no se puede duplicar por accidente
private var cabecera: InlineArray<16, UInt8>
consuming func enviar() { /* consume el paquete: no queda copia viva */ }
borrowing func checksum() -> UInt32 { /* solo lee, no retiene */ 0 }
}
Cada paso de este vector convierte un patrón que antes requería unsafe en un patrón verificado. Ese es el criterio para juzgar cualquier novedad: si reduce la superficie de las cuatro costuras del modelo, es estructural; si solo ahorra teclas, es azúcar.
Concurrencia y macros: la fase ergonómica
El segundo vector es el que más te va a tocar, porque afecta a código que ya has escrito. El vector de concurrencia terminó su fase de corrección con Swift 6: el modo estricto convierte en error de compilación todo riesgo de carrera de datos, con aislamiento por regiones para que el compilador entienda cuándo un valor no compartido puede transferirse aunque no sea Sendable. Lo que vino después no fue más poder sino menos fricción, porque la fase de corrección había dejado un lenguaje seguro y agotador.
Las piezas de esa segunda fase apuntan todas al mismo sitio. Un modo en el que el aislamiento por defecto de un módulo es el actor principal, pensado para aplicaciones de interfaz donde casi todo vive ahí y lo excepcional es salirse. Un comportamiento por defecto en el que una función nonisolated y asíncrona se ejecuta en el contexto de quien la llama en vez de saltar a un hilo cualquiera, eliminando la mitad de los errores de Sendable que aparecían sin motivo aparente. Y un atributo explícito para pedir concurrencia real cuando de verdad la quieres. El efecto neto es una inversión del valor por defecto: antes había que anotar para ser seguro, ahora hay que anotar para ser paralelo.
// El desplazamiento del valor por defecto, en dos lineas
@MainActor final class Vista { var titulo = "" } // antes: anotar para ser seguro
nonisolated func calcular() async -> Int { 0 } // ahora corre donde le llaman
@concurrent func cruzarNumeros() async -> Int { 0 } // pides paralelismo explicito
Las macros siguen su propia curva. Resolvieron el problema del código repetitivo con expansión verificada por tipos en lugar de generación textual, y su límite es económico: cada macro es un proceso de compilación aparte que carga y ejecuta un plugin, y un proyecto con muchas macros paga ese peaje en cada build. El movimiento previsible es hacia menos coste de compilación, mejores diagnósticos dentro del código expandido y mayor capacidad de evaluación en tiempo de compilación. Conviene notar que las macros nacieron para retirar presión del lenguaje: cada vez que una necesidad se resuelve con una macro, deja de ser candidata a sintaxis nueva, y eso mantiene pequeño el núcleo del lenguaje mientras el ecosistema crece.
flowchart TD M[Modelo de seguridad unico] M --> O[Ownership: Span InlineArray vidas verificadas] M --> C[Concurrencia: aislamiento por defecto y menos anotaciones] M --> A[Macros: repeticion eliminada con verificacion] M --> E[Embedded: mismo lenguaje sin runtime] O --> R[Rango completo: de firmware a servidor] C --> R A --> R E --> R style M fill:#cba6f7,color:#11111b style R fill:#a6e3a1,color:#11111b
Embedded: el mismo lenguaje sin runtime
Embedded Swift es un perfil del lenguaje, no un dialecto: compila con el mismo compilador y una bandera, y produce binarios de kilobytes que corren en microcontroladores sin sistema operativo. Para lograrlo renuncia a todo lo que exige metadatos de tipos en tiempo de ejecución: nada de reflexión, nada de Any dinámico, nada de casting condicional a protocolos, y genéricos que deben especializarse por completo en compilación, lo que en la práctica significa que los existenciales de clase abierta quedan fuera.
Lo que conserva es lo que importa: opcionales, enums con valores asociados, genéricos, protocolos, ARC, semántica de valor y seguridad de memoria. Es decir, conserva el modelo mental completo del nivel anterior y quita solo la maquinaria dinámica. Esa es la prueba de que el modelo estaba bien planteado: se puede amputar el runtime sin amputar el lenguaje.
// Perfil embebido: mismo lenguaje, mismas garantias, sin metadatos dinamicos
enum Registro: UInt32 { case control = 0x40021000, estado = 0x40021004 }
func escribir(_ r: Registro, _ v: UInt32) {
UnsafeMutablePointer<UInt32>(bitPattern: UInt(r.rawValue))!.pointee = v
}
// Lo que NO compila aqui, y por que:
// let x: Any = 42 -> requiere metadatos de tipo en runtime
// if let s = x as? P -> requiere casting dinamico
// String(describing: x) -> requiere reflexion
Ese enum con valor bruto, ese opcional forzado y esa función genérica compilan sin runtime porque todo lo que necesitan se resuelve antes de arrancar. La lista de prohibiciones es exactamente la lista de cosas que necesitan preguntar por un tipo mientras el programa corre. Todo lo demás —los genéricos incluidos— sobrevive porque se resuelve antes, en compilación, mediante especialización total. Dicho de otro modo: Embedded Swift no es Swift con menos lenguaje, es Swift sin la parte del lenguaje que exige un intérprete de tipos en tiempo de ejecución.
La consecuencia estratégica va más allá de los microcontroladores. Un Swift capaz de funcionar sin runtime, sin asignador y sin excepciones es un Swift candidato a espacios que hasta ahora eran territorio exclusivo de C: bootloaders, drivers, firmware, componentes de kernel. No es una promesa de que ocurra; es la eliminación del obstáculo técnico que lo impedía.
Qué preparar hoy y en qué orden
La forma equivocada de prepararse es estudiar features que aún no puedes usar. La correcta es dejar el código en el estado en el que las adoptará sin fricción, y eso se consigue con cuatro movimientos que además mejoran el proyecto ahora mismo.
Sanear el aislamiento
Migra módulo a módulo al modo estricto y arregla los avisos por su causa real, no con @unchecked. Cada @unchecked que dejes es deuda que la fase ergonómica no te va a perdonar.
Acotar lo unsafe
Inventaría cada uso de punteros crudos. Los que sean una vista sobre memoria contigua son candidatos directos a Span y desaparecerán casi sin cambios.
Nombrar los recursos
Identifica los tipos que jamás deberían copiarse: conexiones, transacciones, tokens. Son tu lista de futuros ~Copyable, y documentarla hoy ya evita bugs.
Blindar con pruebas
Ninguna migración de modelo de memoria o de concurrencia es segura sin una suite que falle. Las features nuevas se adoptan tan rápido como te lo permita tu red de pruebas.
Adoptar Span o ~Copyable en un módulo que todavía tiene el aislamiento sucio multiplica los frentes: cada error nuevo puede venir de dos causas y ya no sabes de cuál. Primero la concurrencia, después la memoria, siempre con pruebas de por medio. Migrar en paralelo es la forma más fiable de pasar un trimestre sin terminar nada.
Y un quinto movimiento que no es de código: aprende a leer una propuesta en veinte minutos. Motivación, diseño propuesto, alternativas consideradas y compatibilidad de fuentes. Con esas cuatro secciones sabes si el cambio te afecta, y esa capacidad de triaje vale más que memorizar cualquier feature concreta, porque el flujo de propuestas no se va a detener.
Aqui esta el mecanismo que hace posible todo lo anterior y que casi nadie estudia, aunque es probablemente la contribucion de ingenieria mas subestimada del proyecto. Swift tiene ABI estable en las plataformas de Apple y un compromiso publico de compatibilidad de fuentes, lo que significa que un cambio no puede romper ni binarios antiguos ni codigo antiguo. Cualquier lenguaje en esa situacion tiende a la paralisis: C++ arrastra decadas de decisiones que no puede revertir, y Python pago la ruptura de su version tres con casi una decada de ecosistema partido en dos. Swift eligio una tercera via, los modos de lenguaje. Un modo es una version del significado del codigo que se selecciona por modulo, no por proyecto: un mismo binario puede enlazar modulos compilados en modo 5 y en modo 6, cada uno con sus reglas, porque los cambios se disenan de modo que la frontera entre modos siga siendo sana. Eso permite que Swift 6 convierta en error lo que en Swift 5 era aviso sin obligar a nadie a migrar a la vez, y permite migrar un proyecto grande archivo a archivo, midiendo. Encima de los modos se apilan las banderas de features futuras, que activan individualmente un cambio de comportamiento antes de que su modo llegue: sirven para probar hoy lo que sera obligatorio manana, para hacer la migracion incremental y, desde el punto de vista del proyecto, para conseguir datos reales antes de congelar el diseno. Y por debajo de ambos actua el principio rector del que hablan las propuestas una y otra vez, la divulgacion progresiva: una feature nueva no debe aumentar la complejidad que ve quien no la usa. Por eso Span no obliga a nadie a escribir vidas explicitas mientras el compilador pueda inferirlas, por eso el aislamiento por defecto en el actor principal existe justo para que el programador de aplicaciones deje de leer sobre Sendable, y por eso las macros esconden su maquinaria detras de un atributo. La leccion transferible para tu propio trabajo es directa: la pregunta correcta ante cualquier cambio no es solo si es mejor, sino si existe un camino de migracion que no obligue a todos a moverse el mismo dia. Un lenguaje que no puede romperse te ensena eso mejor que ningun libro de arquitectura, porque lo ha tenido que resolver de verdad y en publico.
Cuatro vectores, un objetivo: cubrir de firmware a servidor con un solo modelo de seguridad. Ownership avanza hacia vistas verificadas sin copia, la concurrencia invierte sus valores por defecto, las macros bajan su coste y Embedded demuestra que el modelo sobrevive sin runtime. Los modos de lenguaje son el vehículo que lo hace posible.
- Activa el modo de lenguaje 6 en un módulo pequeño de un proyecto tuyo y anota cada error. Clasifícalos en errores reales y errores de anotación faltante.
- Reescribe una función que hoy use
withUnsafeBufferPointeren términos deSpany compara legibilidad y SIL generado. - Lista las banderas de features futuras que soporta tu toolchain actual, activa una y describe qué cambió.
- Compila un programa mínimo con el perfil embebido y transcribe los errores que produzcan la reflexión y los existenciales. Explica cada uno con el modelo de metadatos en la mano.
- Escribe media página prediciendo qué feature del lenguaje te parece inevitable en dos años y cuál te parece improbable. Fecha el documento y guárdalo para contrastarlo.