wandres.dev
LA MACRO @REDUCER · el boilerplate desaparece

Inspeccionar la expansión de la macro

El código que compilas no es el que lees, y eso convierte la lectura de la expansión en una competencia técnica y no en una curiosidad. Esta lección enseña las tres vías para ver el código generado: el comando de expansión del editor de Xcode, el volcado por línea de órdenes con banderas del compilador, y la navegación al búfer virtual donde el depurador coloca la macro expandida. Después descompone la expansión real de la macro sobre un struct y sobre un enum, y termina con las preguntas concretas que solo la expansión puede responder cuando algo no encaja.

⏱ 19 min

Una macro es un pacto: cedes visibilidad a cambio de brevedad. El pacto sale a cuenta casi siempre, hasta el día en que algo no compila y el mensaje de error habla de un tipo que tú nunca escribiste. En ese momento la única salida es levantar la tapa y mirar el código que la macro emitió, y esa operación no es un truco avanzado sino una habilidad de rutina que Xcode y el compilador de Swift ofrecen de fábrica. Saber leer una expansión cambia tu relación con TCA: dejas de tratar la macro como magia y pasas a tratarla como lo que es, un programa que escribe programas y cuya salida puedes auditar línea por línea.

🎯 Al terminar esta lección sabrás
  • Expandir una macro desde el editor de Xcode y navegar el búfer generado.
  • Volcar todas las expansiones de un módulo desde la línea de órdenes con las banderas del compilador.
  • Leer la expansión de la macro sobre un struct y reconocer cada artefacto emitido.
  • Interpretar la expansión sobre un enum de destinos y localizar el dominio sintetizado.

Tres vías para ver el código generado

La primera vía es la del editor y es la que usarás a diario. Sitúa el cursor sobre el atributo de la macro, abre el menú contextual y elige la opción de expandir la macro. Xcode abre un panel con el código emitido, resaltado y navegable, y desde ahí puedes seguir expandiendo macros anidadas porque la expansión de @Reducer contiene a su vez otras macros. El mismo comando está en el menú del editor, y hay un atajo equivalente que muestra la expansión en línea junto a la declaración original.

La segunda vía es la del compilador y sirve cuando quieres el volcado completo de un módulo, no de una declaración. Con un paquete de Swift basta pasar la bandera al compilador subyacente.

swift build -Xswiftc -Xfrontend -Xswiftc -dump-macro-expansions

El compilador escupe por la salida estándar cada expansión precedida por el nombre del búfer virtual que la contiene, con la forma @__swiftmacro_ seguida del módulo, el tipo, el nombre de la macro y un identificador. Ese nombre no es decorativo: es exactamente el que verás en los mensajes de error y en las trazas de pila cuando algo falle dentro de código generado.

La tercera vía es la del depurador. Como la expansión vive en un búfer real desde el punto de vista del compilador, puedes poner un punto de interrupción dentro de ella, entrar paso a paso desde tu código y ver la pila atravesar declaraciones que nunca escribiste. Es la única forma de responder con certeza a la pregunta de en qué orden corre lo que la macro generó.

💡
Si la opción de expandir no aparece

El menú de expansión solo se ofrece cuando el índice del proyecto está construido y el plugin de la macro está compilado y autorizado. Si la opción está ausente o gris, compila el objetivo una vez, confirma que aceptaste el diálogo de confianza del plugin y espera a que termine la indexación. Una expansión que no aparece casi nunca significa que la macro esté rota; significa que el editor todavía no sabe lo suficiente.

Anatomía de la expansión sobre un struct

Toma la feature más pequeña posible y expándela. La macro es de tipo miembro y extensión a la vez, así que emite código en dos lugares: atributos añadidos a declaraciones existentes y una extensión nueva al final.

@Reducer
struct Contador {
  @ObservableState
  struct State: Equatable { var cuenta = 0 }
  enum Action { case incrementar }
  var body: some ReducerOf<Self> {
    Reduce { state, action in
      state.cuenta += 1
      return .none
    }
  }
}

Lo que verás en el panel, depurado de detalles de calificación completa de nombres, tiene esta forma:

// 1. Atributo añadido al enum de acciones
@CasePathable
@dynamicMemberLookup
enum Action { case incrementar }

// 2. Atributo añadido a la propiedad body
@ReducerBuilder<State, Action>
var body: some ReducerOf<Self> { ... }

// 3. Extensión de conformidad emitida aparte
extension Contador: ComposableArchitecture.Reducer {}

Tres observaciones que solo se aprecian mirando. La primera es que la macro no reescribe tu body: le añade un atributo y deja tu código intacto, lo cual explica por qué los errores dentro de body señalan tus líneas y no líneas generadas. La segunda es que la conformidad llega por extensión separada, y por eso el mensaje de error cuando falta body habla de un requisito de protocolo no satisfecho en lugar de hablar de la macro. La tercera es que los nombres aparecen totalmente calificados con el módulo, un hábito de las macros bien escritas para que tu código no pueda romper la expansión declarando un tipo con el mismo nombre.

flowchart TD
A[Fuente con la macro] --> B[Plugin de macro como proceso aparte]
B --> C[Buffer virtual de expansion]
C --> D[Atributos sobre miembros existentes]
C --> E[Extension de conformidad]
D --> F[Compilacion normal de Swift]
E --> F
F --> G[Diagnosticos que citan el buffer]

La expansión sobre un enum de destinos

Aquí es donde mirar la expansión pasa de instructivo a revelador, porque la relación entre lo que escribes y lo que se genera es de uno a muchos. Un enum de destinos con dos casos produce dos enumeraciones completas, una conformidad y un cuerpo de enrutamiento.

@Reducer
enum Destino {
  case detalle(Detalle)
  case ajustes(Ajustes)
}

La expansión sintetiza, en esencia, un enum State con un caso por destino que envuelve el estado de cada feature, un enum Action simétrico que envuelve sus acciones, la conformidad al protocolo de reducers enumerados, y un body estático que enruta con un Scope por caso. Aproximadamente esto:

enum State {
  case detalle(Detalle.State)
  case ajustes(Ajustes.State)
}

enum Action {
  case detalle(Detalle.Action)
  case ajustes(Ajustes.Action)
}

static var body: some ReducerOf<Self> {
  Scope(state: \.detalle, action: \.detalle) { Detalle() }
  Scope(state: \.ajustes, action: \.ajustes) { Ajustes() }
}

Contar líneas aquí es un ejercicio útil: cuatro líneas escritas producen alrededor de veinte generadas, y esas veinte crecen linealmente con cada destino que añades. Cuando alguien pregunte por qué TCA insiste tanto en las macros, este es el argumento cuantitativo.

Las preguntas que solo la expansión responde

Expandir por curiosidad está bien una vez; expandir con una pregunta concreta es lo que convierte la técnica en herramienta. Hay cuatro preguntas que aparecen una y otra vez en el trabajo real y para las que ninguna otra fuente da una respuesta fiable.

🔎

Existe este case path

Cuando el compilador dice que no encuentra un miembro en tu enum de acciones, la expansión te dice si la anotación llegó a aplicarse o si ese enum quedó fuera del alcance de la macro.

🧭

En qué orden corre

La expansión de un body compuesto muestra la secuencia real de reducers, que es la que determina qué mutación ve cuál. Ninguna documentación puede responderlo por ti.

📐

Qué tipos concretos hay

Detrás de some ReducerOf<Self> hay un tipo concreto anidado que la expansión revela. Es la información que necesitas cuando un error habla de genéricos que no reconoces.

🧾

Qué versión estoy usando

Dos proyectos con la misma línea de código pueden expandir distinto si resolvieron versiones distintas de la librería. La expansión es el único testigo directo de cuál tienes.

Conviene incorporar el gesto a la rutina y no reservarlo para las emergencias. Cuando adoptes un operador nuevo de la librería, expándelo una vez antes de usarlo en serio; media hora invertida en leer lo que genera te ahorra la reconstrucción a ciegas de su comportamiento la primera vez que se tuerza.

ℹ️
La expansión es tu documentación de última instancia

Cuando la documentación de la librería y tu intuición discrepan, la expansión decide. No describe lo que la macro debería hacer ni lo que hacía en una versión anterior: muestra lo que hace en la versión que tienes instalada, con las dependencias que tienes resueltas. Ante cualquier duda sobre comportamiento generado, expandir es más rápido y más fiable que buscar en las notas de la versión.

Leer la expansión es recuperar la propiedad sobre tu propio programa

Hay una incomodidad legítima en las macros que conviene nombrar en vez de reprimir: introducen una brecha entre el texto que un humano lee y el programa que una máquina ejecuta, y toda brecha de ese tipo tiene un coste en comprensibilidad, en depurabilidad y en la capacidad de razonar sobre lo que has construido. Las generaciones anteriores de metaprogramación pagaron ese coste sin recibir nada a cambio del compilador: la generación de código por scripts producía archivos que había que versionar y que se desincronizaban, y los preprocesadores textuales del linaje de C producían diagnósticos que hablaban de un texto que ya no existía. Lo que hace distintas a las macros de Swift, y lo que convierte esta lección en algo más que un truco de herramientas, es que la expansión es un artefacto de primera clase dentro del compilador: tiene un búfer con nombre, se puede abrir, se puede recorrer con el depurador, y los errores apuntan a sus líneas reales. Dicho de otro modo, la brecha existe pero es transparente, y la transparencia convierte el pacto en aceptable. El corolario práctico es que negarte a mirar la expansión no te protege de la complejidad, solo te impide auditarla: el código generado está en tu binario, corre en el dispositivo de tu usuario y responde por tus fallos, seas capaz de leerlo o no. Expandirlo la primera vez cuesta treinta segundos y a cambio te devuelve la posición que ninguna abstracción debería quitarte, la de saber con exactitud qué programa escribiste. Ese es el criterio con el que conviene juzgar cualquier macro que adoptes, propia o ajena: no si es cómoda, sino si puedes ver lo que hace cuando lo necesites.

⚔️ Audita el código que no escribiste
  1. Toma una feature real de tu proyecto y expándela desde el editor. Copia la expansión a un archivo aparte y anota cada artefacto que aparece.
  2. Ejecuta el volcado por línea de órdenes sobre el mismo módulo y localiza el nombre del búfer virtual que corresponde a esa feature.
  3. Borra el body de la feature y compara el error resultante con la expansión. Explica por qué el mensaje habla de un requisito de protocolo y no de la macro.
  4. Escribe un @Reducer enum con tres destinos, expándelo y cuenta las líneas generadas frente a las escritas. Anota la proporción.
  5. Pon un punto de interrupción dentro del código expandido de un Scope y ejecuta una acción del hijo. Describe el orden real en que corren los reducers del body.