wandres.dev
LA MACRO @REDUCER · el boilerplate desaparece

Errores de macros y cómo leerlos

Los errores que produce una macro rara vez señalan la causa: hablan de conformidades que no pediste, de tipos que no escribiste o de plugins que no sabías que existían. Esta lección construye un método de lectura para esos diagnósticos, distingue el fallo de expansión del fallo dentro de lo expandido, cataloga los mensajes más frecuentes de la macro de TCA con su causa real y su remedio, y cierra con el procedimiento a seguir cuando la macro sencillamente no expande: confianza del plugin, cachés corruptas y desajustes de versión de la biblioteca de sintaxis.

⏱ 18 min

El primer error de macro que te encuentras es desconcertante por una razón concreta: no habla de nada que hayas escrito. Dice que tu tipo no conforma un protocolo que la macro debía darle, o que no encuentra una implementación externa cuyo nombre nunca viste, o que un tipo no existe cuando está a tres líneas de distancia. La causa de ese desconcierto es estructural: cuando la generación falla, el compilador no puede describir la ausencia de lo que iba a generarse, así que informa de las consecuencias río abajo. Leer bien esos mensajes consiste en aprender a remontar la corriente, y hay un puñado pequeño de causas reales detrás de casi todos ellos.

🎯 Al terminar esta lección sabrás
  • Distinguir un fallo en la expansión de un fallo dentro del código ya expandido.
  • Traducir los diagnósticos más frecuentes de la macro de TCA a su causa verdadera.
  • Diagnosticar por qué una macro no expande y aplicar el remedio correcto en cada caso.
  • Aplicar un procedimiento fijo de descarte en lugar de probar cambios al azar.

Dos familias de fallo, dos lecturas distintas

Antes de leer un mensaje, clasifícalo. Si la macro no llegó a expandirse, el problema es del entorno de compilación: el plugin no se construyó, no está autorizado, o la versión de la biblioteca de sintaxis no casa con la del compilador. Los mensajes de esta familia mencionan implementaciones externas, plugins o objetivos que deben habilitarse, y ninguno de ellos se arregla tocando tu código.

Si la macro expandió pero el resultado no compila, el problema es de tu fuente: la macro generó lo que sabe generar y ese código chocó con lo que tú escribiste. Los mensajes de esta familia hablan de conformidades, de tipos que faltan o de requisitos no satisfechos, y su rasgo distintivo es que señalan la línea del atributo de la macro aunque la causa esté dentro del cuerpo del tipo. Ese desplazamiento no es un defecto del compilador, es la consecuencia inevitable de que el código culpable pertenezca a un búfer generado.

Hay una tercera situación que conviene descartar antes que ninguna otra porque se disfraza de las dos anteriores: la macro expandió correctamente y el error es tuyo pero no tiene nada que ver con macros. Un error de tipo dentro de un Reduce, una dependencia mal declarada o un caso de switch sin cubrir producen mensajes normales que sencillamente aparecen en un archivo lleno de atributos, y el sesgo de atribución hace que sospeches de la macro. La señal para distinguirla es simple: si el mensaje cita una línea que tú escribiste y describe un problema que entiendes, no es un problema de macro por mucho que haya una macro cerca.

⚠️
No cambies dos cosas a la vez

La disciplina más rentable al depurar generación de código es alterar una sola variable por ciclo de compilación. Si limpias la caché, actualizas la librería y renombras un tipo en el mismo intento, el resultado no te enseña nada aunque compile, porque no sabrás cuál de las tres cosas lo arregló ni podrás reproducir el remedio la próxima vez.

💡
La primera acción ante cualquiera de las dos familias

Expande la macro. Si el panel de expansión se abre y muestra código coherente, estás en la segunda familia y la causa es tuya. Si el panel falla, está vacío o el comando ni siquiera se ofrece, estás en la primera y el problema es del entorno. Ese único gesto reparte el espacio de búsqueda en dos mitades antes de haber tocado nada.

Catálogo de diagnósticos frecuentes

Los mensajes que siguen cubren la abrumadora mayoría de las incidencias reales con la macro de TCA. Interesa memorizar la columna del medio, porque es la que ningún mensaje dice.

Diagnóstico que ves Causa real Remedio
El tipo no conforma al protocolo de reducers No hay ni body ni reduce, o ambos existen y el segundo quedó muerto Define exactamente uno de los dos
No encuentra el tipo State en el ámbito El tipo anidado se llama de otro modo o vive fuera del reducer Renombra a State y anídalo dentro del tipo
No encuentra el miembro dinámico en el enum de acciones Un enum ajeno a la macro al que le falta @CasePathable Anota ese enum a mano
No puede convertir el key path al tipo esperado en un Scope Coordenadas cruzadas o campo que no pertenece al hijo Verifica que el primer argumento es estado y el segundo acción
El compilador no puede verificar la expresión en un tiempo razonable body con demasiados reducers apilados para el result builder Extrae subcomposiciones a propiedades con tipo anotado
No encuentra la implementación externa de la macro El plugin no se compiló o la caché está corrupta Limpia la carpeta de compilación y reconstruye
El objetivo debe habilitarse antes de usarse El plugin no está autorizado en el editor Acepta el diálogo de confianza del plugin

Dos entradas merecen comentario. La del tiempo de verificación aparece cuando el body acumula muchos operadores encadenados y el result builder genera un árbol de sobrecargas demasiado grande; la solución nunca es simplificar la lógica sino romper el body en piezas con tipo explícito, típicamente propiedades que devuelven some ReducerOf<Self>. Y la del miembro dinámico se da casi siempre en enums que no son acciones de una feature: recuerda que la macro solo anota el enum llamado Action del tipo que decora, y ninguno más.

// Romper un body demasiado grande en piezas con tipo anotado
@Reducer
struct Grande {
  @ObservableState struct State: Equatable { var a = 0 }
  enum Action { case algo }

  var body: some ReducerOf<Self> {
    nucleo
      .ifLet(\.$hoja, action: \.hoja) { Hoja() }
  }

  private var nucleo: some ReducerOf<Self> {
    Reduce { state, action in
      return .none
    }
  }
}

Cuando la macro no expande

Esta es la familia que más tiempo hace perder porque invita a modificar código que estaba bien. El orden de descarte importa y conviene seguirlo sin saltarse pasos.

🔐

Confianza del plugin

Una macro es un ejecutable que corre en tu máquina, así que el editor pide autorización explícita la primera vez. Si aparece un aviso pidiendo habilitar el objetivo, acéptalo; hasta entonces la macro no expande y todo lo que dependa de ella falla.

🧹

Caché corrupta

Tras cambiar de rama, actualizar la biblioteca o interrumpir una compilación, la caché puede conservar un plugin a medio construir. Limpia la carpeta de compilación, reinicia las cachés de paquetes y reconstruye desde cero antes de sospechar de nada más.

🧩

Versiones desajustadas

El plugin se compila contra la biblioteca de sintaxis de Swift, y esa dependencia debe casar con el compilador que usas. Un editor recién actualizado con una resolución de paquetes antigua produce fallos de expansión que ningún cambio en tu código arregla.

Hay dos comprobaciones adicionales que cierran los casos restantes. La primera es trivial y sorprendentemente común: falta la importación de la biblioteca, y sin ella el atributo de la macro no se resuelve. La segunda es que la primera compilación de un proyecto con macros construye la biblioteca de sintaxis entera y tarda varios minutos, así que un editor que parece colgado la primera vez suele estar trabajando, no roto.

flowchart TD
A[Error relacionado con la macro] --> B[Intenta expandir el panel]
B --> C[Panel coherente igual a fallo en tu fuente]
B --> D[Panel vacio igual a fallo del entorno]
C --> E[Revisa nombres State y Action]
C --> F[Revisa body o reduce]
C --> G[Revisa coordenadas de Scope]
D --> H[Autoriza el plugin]
D --> I[Limpia caches y reconstruye]
D --> J[Revisa versiones y la importacion]

El procedimiento de descarte

La tentación ante un error opaco es cambiar cosas hasta que compile, y esa estrategia funciona lo bastante a menudo como para consolidarse y lo bastante poco como para arruinarte una tarde. La alternativa es un procedimiento fijo, siempre el mismo, que en cinco pasos agota el espacio de causas sin depender de la intuición.

El primer paso es aislar la primera línea de error real. Un fallo de macro produce cascadas de diez o quince mensajes de los cuales solo el primero tiene información; los demás son consecuencias de que el tipo quedó mal formado. Filtra la lista y trabaja únicamente sobre el diagnóstico más temprano en orden de archivo y línea.

El segundo es intentar la expansión, que como vimos reparte el problema en las dos familias. El tercero, si el fallo es de tu fuente, es verificar las cuatro expectativas de la macro en este orden literal: existe un tipo anidado llamado State, existe otro llamado Action, existe exactamente uno entre body y reduce, y la importación de la biblioteca está presente. Ese orden no es arbitrario: va de la causa más frecuente a la menos.

// Caso mínimo de descarte: si esto compila, el entorno está sano
@Reducer
struct Sonda {
  @ObservableState
  struct State: Equatable { var x = 0 }
  enum Action { case toco }
  var body: some ReducerOf<Self> {
    Reduce { state, action in
      state.x += 1
      return .none
    }
  }
}

El cuarto paso es la sonda: pega ese fragmento mínimo en un archivo nuevo del mismo objetivo. Si compila, el entorno está sano y la causa está en tu feature; si no compila, la causa está en el entorno y ningún cambio en tu feature la arreglará. Es la comprobación más barata que existe y descarta la mitad del espacio de búsqueda en un ciclo de compilación.

El quinto es la bisección: si la sonda compila y tu feature no, copia la sonda y ve trasplantando piezas de tu feature de una en una, empezando por el estado, siguiendo por las acciones y terminando por el body. La pieza que rompe la sonda es la causa, sin margen de duda y sin necesidad de haber entendido el mensaje.

ℹ️
Reduce el caso antes de pedir ayuda

Cuando un error resista el procedimiento, aísla la feature en un archivo nuevo con el mínimo indispensable: un estado con un campo, una acción con un caso y un body vacío. Si el mínimo compila, reintroduce piezas de una en una hasta que rompa; la pieza que rompe es la causa. Ese descarte mecánico es más rápido que cualquier intuición y produce, de paso, el ejemplo reproducible que necesitarías para abrir una incidencia.

Un error mal ubicado sigue siendo información, si sabes de dónde viene

La frustración con los diagnósticos de macros nace de una expectativa razonable que las macros no pueden cumplir: que el compilador señale la causa. Un compilador señala el lugar donde el programa deja de tener sentido, y con metaprogramación ese lugar y la causa se separan por construcción, porque entre ambos hay un programa intermedio que decidió qué escribir. Cuando la macro genera una conformidad y tu tipo no cumple los requisitos que esa conformidad implica, el error verdadero es que la macro esperaba algo de tu fuente y no lo encontró; lo que el compilador puede decir, en cambio, es que un protocolo quedó insatisfecho. La distancia entre ambas frases es el precio de la generación de código, y ninguna mejora en los mensajes lo elimina del todo, solo lo acorta. La consecuencia práctica es que el oficio de depurar macros no consiste en leer mejor los mensajes sino en construir un modelo de qué espera la macro de ti, para poder traducir cualquier síntoma a la expectativa incumplida que lo provocó. Ese modelo es exactamente lo que han ido construyendo las cuatro lecciones anteriores de este bloque: la macro espera un tipo anidado llamado State, otro llamado Action, exactamente uno entre body y reduce, y un plugin autorizado que pueda ejecutarse. Cuatro expectativas, y prácticamente todos los errores que verás en tu vida con TCA son alguna de las cuatro incumplida, contada de una manera que no lo parece. Quien conoce esa lista deja de depurar por prueba y error y empieza a depurar por descarte, que es la diferencia entre una tarde perdida y tres minutos.

⚔️ Provoca cada error a propósito y catalógalo
  1. Parte de una feature que compile. Borra el body y anota el mensaje literal. Añade luego un reduce junto al body y anota el aviso que aparece.
  2. Renombra State a Estado y captura todos los errores que se encadenan. Cuenta cuántos mensajes produce una sola causa.
  3. Cruza los argumentos de un Scope y guarda el diagnóstico. Compáralo con el del paso anterior y decide cuál tenía más información útil.
  4. Apila quince operadores en un body hasta provocar el fallo de tiempo de verificación, y arréglalo extrayendo una propiedad con tipo anotado.
  5. Construye tu propia tabla de tres columnas con los mensajes que has capturado, su causa real y el remedio. Guárdala: será tu procedimiento de descarte durante el resto del track.