Por qué TEA inspiró a Redux, TCA y MVI
The Elm Architecture no se quedó en Elm: se convirtió en el patrón que otras comunidades redescubrieron una y otra vez. Esta lección mapea la herencia con precisión. Redux, que Dan Abramov reconoce inspirado en Elm, traduce update a reducer y Msg a action, pero deja fuera los efectos gestionados y tiene que recuperarlos con middleware. TCA, en Swift, es casi un port literal: State, Action, Reducer y hasta el tipo Effect que preserva los Cmd de Elm. MVI, en Android, renombra Msg a Intent y conserva el flujo unidireccional. La tesis final es que las tres son la misma idea —estado inmutable más transición pura basta para construir cualquier UI— demostrada en tres lenguajes distintos, y que TEA importa porque es esa idea en su forma más limpia.
Elm casi nadie lo despliega en producción, y sin embargo su arquitectura está por todas partes. Eso solo se explica de una manera: The Elm Architecture no era una característica de Elm, sino una idea que Elm supo aislar en su forma más pura, y las ideas puras viajan solas. Redux la llevó a JavaScript y conquistó el frontend de una década. TCA la llevó a Swift casi sin cambiarle una coma. MVI la llevó a Android con otro vocabulario. Ninguna de las tres cita casualmente a Elm: las tres reconocen, con más o menos honestidad, que están reimplementando el mismo patrón. Esta lección traza el árbol genealógico con nombres y correspondencias exactas, y termina defendiendo por qué todas son, en el fondo, el mismo teorema demostrado en tres lenguajes.
- Mapear Redux sobre TEA:
reduceresupdate,actionesMsg, y qué dejó fuera. - Reconocer TCA como un port casi literal, incluido el tipo
Effectque preservaCmd. - Situar MVI en Android:
IntentesMsgy el flujo unidireccional se conserva. - Defender la tesis común: estado inmutable más transición pura basta para cualquier UI.
Redux: TEA en JavaScript, menos los efectos
Dan Abramov, autor de Redux, ha reconocido en repetidas ocasiones que Elm fue su inspiración directa. El mapeo es casi uno a uno: el Msg de Elm es la action de Redux, el update de Elm es el reducer de Redux, y el Model único es el state del store único. La misma firma pura, la misma unidireccionalidad, el mismo estado inmutable.
// El reducer de Redux es el update de Elm con otra sintaxis:
// misma firma pura (estado, accion) -> estado nuevo
function reducer(estado = { cuenta: 0 }, accion) {
switch (accion.type) {
case "INCREMENTAR":
return { ...estado, cuenta: estado.cuenta + 1 }
default:
return estado
}
}
Pero Redux dejó fuera una pieza esencial: los efectos gestionados. En Elm, update devuelve (Model, Cmd Msg) y el Cmd es un ciudadano de primera clase del patrón. El reducer de Redux, en cambio, devuelve solo el estado; no hay un canal nativo para describir efectos. La comunidad tuvo que reconstruir esa mitad ausente a mano, con middleware —redux-thunk, redux-saga, el listener middleware—, precisamente el tema del nivel 20. Lo que en Elm venía soldado de fábrica, Redux lo dejó como ejercicio para el lector.
El Cmd de Elm no es un detalle: es la respuesta del patrón a “¿cómo hago algo impuro sin ensuciar la transición pura?”. Redux copió la transición pura pero no su respuesta a esa pregunta, y por eso el ecosistema de Redux gastó años debatiendo dónde y cómo meter los efectos —thunks contra sagas contra observables—. Ese debate no existe en Elm, donde la respuesta viene dada. Cuando una copia deja fuera una pieza del original, esa pieza no desaparece: reaparece como el problema abierto de la copia.
TCA: el port casi literal a Swift
Si Redux es una traducción con licencias, The Composable Architecture —TCA, de Point-Free— es casi una transliteración de Elm a Swift. Las correspondencias no se disimulan: State es el Model, Action es el Msg, Reducer es el update, y —aquí está la fidelidad clave— TCA conserva el tipo Effect, que es exactamente el Cmd Msg de Elm: un valor que describe un efecto para que el runtime lo ejecute y devuelva su resultado como otra Action.
// TCA en Swift: Reducer con State y Action, y Effect como el Cmd de Elm
struct Contador: Reducer {
struct State { var cuenta = 0 }
enum Action { case incrementar, cargar, recibido(String) }
func reduce(into state: inout State, action: Action) -> Effect<Action> {
switch action {
case .incrementar:
state.cuenta += 1
return .none
case .cargar:
// como el Cmd de Elm: describe el efecto, no lo ejecuta aqui
return .run { enviar in await enviar(.recibido(try await pedir())) }
case let .recibido(texto):
return .none
}
}
}
TCA entendió lo que Redux dejó pasar: que los efectos gestionados no son opcionales, sino la mitad del patrón. Por eso preservó el Effect como tipo de primera clase, y por eso su modelo mental le resulta inmediatamente familiar a cualquiera que venga de Elm. Es la prueba de que el patrón sobrevive intacto al cambio de lenguaje: cambian struct por record y enum por type, pero la estructura es idéntica.
MVI: el mismo bucle en Android
En el mundo Android y Kotlin, la misma idea circula con el nombre de MVI —Model-View-Intent—, popularizada por librerías como Orbit y Mavericks. El vocabulario cambia pero el esqueleto no: el Intent es el Msg (la intención del usuario expresada como dato), el Model o State es el estado inmutable, y la vista es una función del estado. El flujo unidireccional se conserva punto por punto: un Intent entra, un reductor produce un State nuevo, la vista lo observa y se redibuja.
El nombre “Intent” es una elección afortunada porque subraya lo que ya vimos con Msg: lo que viaja no es una orden que muta la pantalla, sino la intención declarada del usuario, un hecho que el reductor interpretará. Que la comunidad Android eligiera esa palabra sin coordinarse con Elm ni con Point-Free dice mucho: al modelar la interacción como un dato con nombre, uno acaba llamándolo casi inevitablemente “mensaje” o “intención”, porque eso es lo que conceptualmente es. En Kotlin, el estado suele expresarse con data class inmutables y el reductor con un when exhaustivo sobre una sealed interface de intents —el mismo par estado-inmutable-más-transición-exhaustiva que el record y el case de Elm—. Hasta la exhaustividad del case reaparece: el when de Kotlin sobre una jerarquía sellada obliga, como el compilador de Elm, a cubrir todas las intenciones posibles.
flowchart TD TEA[The Elm Architecture] --> RX[Redux en JavaScript] TEA --> TCA[TCA en Swift] TEA --> MVI[MVI en Android] RX --> RX1[action es Msg y reducer es update] RX --> RX2[efectos fuera con middleware] TCA --> TCA1[Action State Reducer y Effect casi literal] MVI --> MVI1[Intent es Msg y flujo unidireccional] style TEA fill:#cba6f7,color:#11111b style TCA1 fill:#a6e3a1,color:#11111b
Tres continentes del desarrollo —web, iOS, Android— llegaron de forma independiente a la misma silueta. Cuando una estructura reaparece así, sin que las comunidades se copien entre sí de manera directa, deja de ser una moda y empieza a parecerse a un descubrimiento.
Conviene registrar también las variantes menores, porque confirman la regla en vez de romperla. El propio ecosistema de Redux paró de escribir reducers a mano y adoptó Redux Toolkit, que con createSlice e Immer te deja expresar la transición como si mutaras, aunque por debajo siga produciendo estado nuevo: cambia la ergonomía, no el patrón. En React, useReducer es TEA en miniatura dentro de un componente. En Kotlin, librerías como Orbit formalizan el Intent, el reductor y el efecto con casi los mismos nombres que TCA. Ninguna de estas variantes contradice la silueta; todas la decoran. Y esa resistencia a deformarse —que puedas cambiarle la sintaxis, el lenguaje y hasta el estilo de mutación aparente sin alterar la estructura— es la señal más fiable de que debajo hay una forma, y no un capricho.
La tesis: una idea, tres demostraciones
Todo lo anterior converge en una sola afirmación, y conviene enunciarla sin adornos: un estado inmutable más una función de transición pura basta para construir cualquier interfaz. No hace falta data-binding bidireccional, ni observadores mutuos, ni objetos que se escriben entre sí. Con un valor que representa todo el estado y una función pura que lo hace avanzar, más un runtime que cierre el bucle y ejecute los efectos descritos como datos, se puede expresar cualquier aplicación interactiva. Redux, TCA y MVI son tres pruebas de ese mismo enunciado, escritas en tres lenguajes.
Elm / TEA
El original. Model, Msg, update, view y los efectos como Cmd. La forma más limpia porque el lenguaje puro la hace obligatoria.
Redux
reducer = update, action = Msg, store único. Dejó fuera los efectos y los recuperó con middleware.
TCA
Casi literal en Swift: State, Action, Reducer y el tipo Effect que preserva el Cmd de Elm.
MVI
En Android: Intent = Msg, estado inmutable, flujo unidireccional. Mismo bucle, otro vocabulario.
La manera madura de entender lo que acabas de ver no es “Elm influyó en varias librerías”, sino algo bastante más fuerte: TEA es la forma normal del estado de interfaz, en el mismo sentido en que una fracción tiene una forma irreducible a la que siempre puedes llegar. Cuando tomas en serio dos exigencias —que el estado sea inmutable y que la transición sea pura— el espacio de arquitecturas posibles colapsa hasta un único punto, y ese punto es TEA. Por eso comunidades que no se leían entre sí aterrizaron en la misma estructura: no se estaban copiando, estaban simplificando el mismo problema con las mismas reglas y no había otro sitio al que llegar. Esto explica el fenómeno más curioso del tema: que Elm, un lenguaje de nicho que casi nadie usa en producción, sea culturalmente uno de los más influyentes de su generación. Su aportación no fue una herramienta, sino una demostración: enseñó, en la forma más desnuda posible porque el lenguaje puro no admitía concesiones, que estado inmutable más transición pura basta, y que todo lo demás —los efectos, las suscripciones, el render eficiente— se puede añadir como datos sobre esa base sin corromperla. Redux, TCA y MVI son esa demostración reescrita en lenguajes impuros, y cada uno pagó un precio distinto por la impureza de su anfitrión: Redux tuvo que atornillar los efectos con middleware, TCA tuvo que construir un Effect que Elm regalaba, MVI tuvo que imponer con disciplina el flujo que Elm hacía cumplir con el compilador. La lección para tu carrera trasciende cualquiera de estas librerías: cuando encuentres la siguiente arquitectura de estado de moda —y la habrá, con otro nombre y otro logo—, no la estudies como algo nuevo, pregúntate dónde está su Model, dónde su Msg, dónde su transición pura y dónde manda los efectos, y casi siempre descubrirás que estás ante la misma idea de 2012 con ropa nueva. Quien reconoce la forma normal deja de coleccionar frameworks y empieza a ver, debajo de todos ellos, el único teorema que de verdad tuvo que aprender.
- Toma un
reducerde Redux tuyo y traduce a mano cada parte a Elm:actionaMsg, el cuerpo aupdate, elstateaModel. Anota qué correspondencia no encaja y por qué (pista: los efectos). - Busca dónde declara tu app de Redux sus efectos —thunk, saga, listener— y explica por qué esa pieza no tiene equivalente dentro del
reducerpero sí dentro delupdatede Elm. - Si conoces Swift, lee un
Reducerde TCA y señala suState, suActiony suEffect. Compáralo conModel,MsgyCmd. Mide cuánto se parecen. - Escribe en una frase la tesis común —“estado inmutable más transición pura basta para…”— y complétala. Luego argumenta si crees que es cierta para toda UI o si ves un contraejemplo.
- Elige una arquitectura de estado que no se haya mencionado aquí y diseca su anatomía: ¿dónde está su
Model, suMsg, su transición pura y su gestión de efectos? Decide si es una instancia más de la forma normal.