wandres.dev
TCA: EL MODELO · Point-Free en Swift

Su herencia: de Redux y Elm a Swift

TCA no inventa el flujo unidireccional: lo hereda de la arquitectura de Elm y de Redux, y lo traduce a Swift aprovechando dos rasgos del lenguaje que la web no tenía —los tipos algebraicos y la semántica de valor—. Esta lección traza el linaje con precisión: qué toma de Elm (el trío modelo, mensaje y update, todo puro), qué toma de Redux (un store único, acciones como dato plano y un reducer que transforma el estado), y qué añade Swift que hace la traducción mejor que el original (el `enum` exhaustivo para modelar `Action`, el `struct` con semántica de valor para `State`, y la posibilidad de hacer irrepresentables los estados imposibles). El objetivo es entender que TCA no es una moda sino la enésima encarnación de una idea de cuarenta años, ahora expresada en un sistema de tipos que la hace más honesta.

⏱ 17 min

Ninguna de las buenas ideas de TCA es suya. El flujo unidireccional viene de Elm; el store único y el reducer vienen de Redux; la separación entre describir un cambio y ejecutarlo es tan vieja como la programación funcional. Lo que hace TCA no es inventar, sino traducir: toma un patrón probado durante una década en la web y lo reescribe en Swift, aprovechando dos rasgos que ni Elm ni JavaScript tenían a la vez —los tipos algebraicos y la semántica de valor—. Esta lección recorre esa herencia con nombre y apellido, porque entender de dónde sale cada pieza es entender por qué tiene la forma que tiene, y por qué la versión Swift es, en varios aspectos, más rigurosa que sus ancestros.

🎯 Al terminar esta lección sabrás
  • Identificar qué hereda TCA de Elm: el modelo puro, el mensaje y la función update.
  • Identificar qué hereda de Redux: el store único, la acción como dato y el Reducer.
  • Ver qué aporta Swift: el enum exhaustivo para Action y el struct de valor para State.
  • Entender por qué los tipos algebraicos hacen irrepresentables los estados imposibles.

Lo que hereda de Elm

Elm, el lenguaje funcional para la web, propuso a mediados de la década pasada un patrón tan limpio que acabó llamándose simplemente The Elm Architecture. Son tres piezas: un Model que contiene todo el estado, un Message que describe cada cosa que puede pasar, y una función update que, dado un mensaje y el modelo actual, devuelve el modelo siguiente. La clave es que update es pura: no muta nada del mundo, solo calcula un valor nuevo a partir de los que recibe. Los efectos —peticiones de red, temporizadores— no se ejecutan dentro de update, sino que se devuelven como una descripción que el runtime de Elm interpreta después.

TCA copia ese esqueleto casi punto por punto. El Model de Elm es el State de TCA; el Message es el Action; la función update es el Reducer; y la idea de devolver los efectos en vez de ejecutarlos es el Effect. Si conoces Elm, ya conoces la forma de TCA; solo cambia el vocabulario y el lenguaje anfitrión. Esa deuda es explícita: Point-Free ha citado a Elm como inspiración directa en incontables ocasiones.

Lo que hereda de Redux

Redux llevó la arquitectura de Elm al ecosistema de JavaScript y le dio el vocabulario que hoy es común. De Redux, TCA toma tres compromisos. El primero es el store único: una sola fuente de verdad que sostiene el estado de la aplicación, frente a un estado repartido en mil objetos. El segundo es la acción como dato plano: un cambio no se pide llamando a un método, se describe creando un valor que dice qué pasó, y ese valor se puede registrar, inspeccionar y reproducir. El tercero es el reducer como transformación pura: una función que recibe el estado y la acción y devuelve el estado siguiente, sin efectos ocultos.

// La firma conceptual es la misma de Redux, escrita en Swift:
// (inout State, Action) -> Effect<Action>
var body: some ReducerOf<Self> {
  Reduce { state, action in
    switch action {
    case .botonMasPulsado:
      state.cuenta += 1
      return .none          // sin efecto: solo cambia el estado
    case .respuestaNumero(let n):
      state.cuenta = n
      return .none
    }
  }
}

Hay una diferencia de forma que merece atención. En Redux, el reducer recibe el estado y devuelve una copia nueva —(state, action) => newState—. En TCA, el reducer recibe el estado como inout y lo muta en el sitio —(inout State, Action) -> Effect—. Parece una traición a la pureza, pero no lo es: gracias a la semántica de valor de Swift, mutar un inout struct es indistinguible de devolver una copia modificada, y el compilador garantiza que nadie más ve esa mutación a medias. Se gana la ergonomía de escribir state.cuenta += 1 sin perder la garantía de que el reducer es una función pura del estado y la acción.

Lo que aporta Swift

Aquí es donde la traducción supera al original. Redux vive en JavaScript, un lenguaje sin tipos, donde una acción es un objeto con una cadena en el campo type y el reducer es un switch sobre esa cadena que el lenguaje no comprueba. Si te olvidas de un caso, nada te avisa. TCA vive en Swift, y modela Action como un enum: un tipo algebraico cerrado cuyos casos el compilador conoce. El switch del Reducer sobre ese enum es exhaustivo por obligación del lenguaje —si añades un caso y olvidas manejarlo, el código no compila—. La misma disciplina que Redux pedía por convención, Swift la exige por tipo.

// Action es un enum: cada evento posible es un caso, y el switch debe cubrirlos todos.
enum Action: Equatable {
  case aparecer
  case botonMasPulsado
  case respuestaNumero(Int)
  case fallo(String)
}

El segundo regalo de Swift es la capacidad de hacer irrepresentables los estados imposibles. Con struct para agrupar y enum para las alternativas excluyentes, el State puede diseñarse de modo que las combinaciones ilegales ni siquiera se puedan escribir. En lugar de tener a la vez cargando: Bool, datos: [Item]? y error: String? —que permite el absurdo de estar cargando y con error al mismo tiempo—, se modela un enum con casos cargando, cargado([Item]) y fallo(String), y el compilador prohíbe el estado imposible. Es la lección del Nivel 9 aplicada a la arquitectura entera.

🌳

De Elm: el esqueleto

Model, Message y update puros se convierten en State, Action y Reducer. Los efectos se devuelven, no se ejecutan.

🔴

De Redux: el vocabulario

Store único como fuente de verdad, la acción como dato plano inspeccionable, y el reducer como transformación pura del estado.

🧮

De Swift: los tipos algebraicos

El enum hace Action exhaustivo y comprobado por el compilador; el switch incompleto ni siquiera compila.

💎

De Swift: la semántica de valor

El struct como State permite mutar con inout sin perder pureza: cada instantánea es una copia independiente y honesta.

flowchart TD
ELM[Elm: Model Message update] --> TCA[TCA: State Action Reducer Effect]
REDUX[Redux: store accion reducer] --> TCA
SWIFT[Swift: enum struct valor semantico] --> TCA
TCA --> R[flujo unidireccional con tipos fuertes]
style TCA fill:#cba6f7,color:#11111b
style ELM fill:#89b4fa,color:#11111b
style REDUX fill:#89b4fa,color:#11111b
style SWIFT fill:#f9e2af,color:#11111b
style R fill:#a6e3a1,color:#11111b
La semántica de valor es lo que hace honesto al reducer

El detalle más sutil de esta herencia, y el que separa a TCA de una copia mecánica de Redux, es qué significa que State sea un struct y no una class. Un struct en Swift tiene semántica de valor: cuando lo copias, obtienes una instancia independiente, y cuando lo mutas, nadie que tuviera una copia anterior ve el cambio. Esa propiedad, que parece un detalle de rendimiento, es en realidad la base filosófica de toda la arquitectura. Cuando el Reducer recibe inout State y escribe state.cuenta += 1, no está compartiendo un objeto mutable con el resto del sistema —el pecado que hunde a las arquitecturas basadas en referencias—; está trabajando sobre un valor que le pertenece en exclusiva durante esa transición, y al terminar entrega un estado nuevo que es una fotografía completa e inmutable del mundo. Por eso TCA puede guardar el historial de estados, reproducir una sesión paso a paso o comparar el antes y el después en un test sin miedo a que una referencia compartida corrompa el pasado: cada estado es un valor congelado, no un objeto vivo que sigue cambiando bajo tus pies. Redux tuvo que pedir la inmutabilidad por disciplina, rogando a los desarrolladores que no mutaran el estado y añadiendo librerías como Immer para forzarlo; Swift la regala en el tipo. Esta es la razón profunda por la que la traducción de TCA no es una imitación tardía sino, en un sentido preciso, una mejora del original: el mismo patrón, apoyado por fin en un lenguaje cuyo modelo de valores encaja con lo que el patrón siempre necesitó.

⚔️ Traza el linaje sobre una feature
  1. Escribe el State de una feature como un struct y enumera sus campos. Confirma que es un valor: ¿copiarlo produce una instancia independiente?
  2. Modela su Action como un enum con un caso por cada evento. Añade un caso nuevo a propósito y comprueba que el switch del Reducer deja de compilar hasta que lo manejas.
  3. Busca en tu State una combinación de campos que represente un estado imposible —por ejemplo, cargando y con error a la vez— y rediséñalo con un enum que lo haga irrepresentable.
  4. Compara la firma de tu Reducer(inout State, Action) -> Effect— con la de un reducer de Redux —(state, action) => newState— y explica por qué la mutación con inout sigue siendo pura.
  5. Nombra en una frase qué toma esta feature de Elm, qué de Redux y qué solo es posible gracias a Swift.