wandres.dev
THE ELM ARCHITECTURE · Model, update, view

Las piezas: Model, Msg, update y view

The Elm Architecture se sostiene sobre cuatro piezas y ni una más: Model es el estado completo de la aplicación como un único valor inmutable; Msg es un tipo suma que enumera de forma cerrada todo lo que puede ocurrir; update es la transición pura con firma Msg a Model a Model; y view es el render puro con firma Model a Html Msg. Esta lección disecciona cada pieza en Elm actual, muestra su código idiomático con Browser.sandbox, y explica por qué el hecho de que Msg sea un tipo suma cerrado y el compilador exija exhaustividad convierte la lista de mensajes en una especificación viva del comportamiento posible.

⏱ 17 min

Toda The Elm Architecture cabe en cuatro nombres: Model, Msg, update y view. No hay una quinta pieza escondida ni una capa de configuración que se te olvidó leer; el patrón es literalmente eso y nada más. Pero la sobriedad engaña. Cada una de las cuatro carga con una responsabilidad exacta y con una prohibición igual de exacta, y el poder del conjunto no está en lo que cada pieza hace, sino en cómo sus tipos encajan hasta formar una tenaza en la que el compilador te obliga a decir la verdad. Vamos a abrir las cuatro, ver su código real en Elm y entender por qué el tipo de Msg, en concreto, es la pieza más subestimada de todo el patrón.

🎯 Al terminar esta lección sabrás
  • Definir Model como el estado completo de la app en un único valor inmutable.
  • Entender Msg como un tipo suma cerrado que enumera todo lo que puede ocurrir.
  • Leer update como la transición pura de firma Msg -> Model -> Model.
  • Situar view como render puro de firma Model -> Html Msg que solo emite mensajes.

Model: el estado como un solo valor

El Model es el estado completo de la aplicación condensado en un único valor inmutable, casi siempre un record. No hay estado repartido en variables sueltas, en atributos del DOM ni escondido dentro de componentes: si algo forma parte del estado, vive en el Model y en ningún otro sitio. Esa es la fuente única de verdad llevada a su forma más literal.

type alias Model =
    { cuenta : Int
    , nombre : String
    , cargando : Bool
    }

inicial : Model
inicial =
    { cuenta = 0, nombre = "", cargando = False }

Que sea un solo valor inmutable tiene una consecuencia inmediata: en cualquier instante puedes fotografiar el estado entero con una sola lectura, serializarlo, compararlo con el de hace un segundo o guardarlo para reproducirlo después. No hay que recolectar el estado de doce sitios porque nunca estuvo repartido.

Msg: el catálogo cerrado de lo que puede pasar

Msg es un tipo suma —una unión etiquetada— que enumera, uno por uno, todos los eventos que tu aplicación reconoce. No es un objeto con un campo type de texto libre; es un tipo cerrado donde cada variante puede llevar sus propios datos, y donde no existe ningún mensaje fuera de la lista.

type Msg
    = Incrementar
    | Decrementar
    | NombreCambiado String
    | Reiniciar

Leer ese bloque es leer, de un vistazo, absolutamente todo lo que puede ocurrirle a tu aplicación. Cuatro cosas y ni una más. Esta propiedad —que el conjunto de eventos posibles sea finito, explícito y revisable— es lo que convierte la definición de Msg en una especificación viva: no un comentario que se desactualiza, sino código que el compilador mantiene honesto.

Fíjate además en que algunas variantes cargan datos y otras no. Incrementar y Reiniciar son eventos sin carga —basta con que hayan ocurrido—, mientras que NombreCambiado String transporta el texto nuevo del campo. Esa capacidad de que cada mensaje lleve exactamente los datos que su caso necesita, y ni uno más, es lo que distingue un tipo suma de un simple enum de constantes: no describes solo qué clase de evento pasó, sino con qué información llegó. En update recuperarás esos datos por coincidencia de patrones, con la garantía de que el tipo te da el dato de cada variante sin castings ni comprobaciones defensivas.

update: la transición pura

update recibe un mensaje y el estado actual, y devuelve el estado siguiente. Su firma, Msg -> Model -> Model, es la tesis entera de la pieza: dos entradas, una salida, y nada más entra ni sale. Se implementa con un case sobre el Msg, y aquí aparece la tenaza del compilador: si el case no cubre todas las variantes de Msg, el programa no compila.

update : Msg -> Model -> Model
update msg model =
    case msg of
        Incrementar ->
            { model | cuenta = model.cuenta + 1 }

        Decrementar ->
            { model | cuenta = model.cuenta - 1 }

        NombreCambiado texto ->
            { model | nombre = texto }

        Reiniciar ->
            inicial

Fíjate en { model | cuenta = model.cuenta + 1 }: no muta model, construye un record nuevo que comparte lo no tocado. El estado anterior sigue existiendo, intacto. Y si mañana añades una variante Duplicar al tipo Msg, el compilador señalará este case como incompleto hasta que la trates. La exhaustividad no es un test que puedas olvidar escribir; es una condición para que el código exista.

view: el render puro que solo habla en Msg

view toma el Model y devuelve una descripción de la interfaz, Html Msg. Es una función pura: para el mismo modelo produce siempre el mismo árbol. Y su tipo de retorno esconde una restricción crucial: Html Msg significa que lo único que la vista puede emitir hacia el sistema son valores de tipo Msg. No puede llamar a update, no puede tocar el modelo, no puede ejecutar un efecto. Solo puede decir “cuando el usuario haga clic, emite este Msg”.

view : Model -> Html Msg
view model =
    div []
        [ button [ onClick Decrementar ] [ text "-" ]
        , text (String.fromInt model.cuenta)
        , button [ onClick Incrementar ] [ text "+" ]
        , input [ onInput NombreCambiado, value model.nombre ] []
        ]

Las cuatro piezas se ensamblan sin pegamento adicional. En Elm actual, para una app sin efectos, Browser.sandbox las une:

main : Program () Model Msg
main =
    Browser.sandbox
        { init = inicial
        , update = update
        , view = view
        }
📦

Model

El estado completo en un único valor inmutable, normalmente un record. Fuente única de verdad: si es estado, vive aquí y solo aquí.

✉️

Msg

Un tipo suma cerrado que enumera todo lo que puede pasar. Cada variante lleva sus datos. Fuera de la lista no existe ningún evento.

🔄

update

La transición pura Msg -> Model -> Model. Un case exhaustivo: el compilador te obliga a tratar cada Msg.

🖥️

view

El render puro Model -> Html Msg. Solo lee el modelo y solo emite mensajes; jamás muta ni ejecuta efectos.

El tipo de Msg es una especificación ejecutable del comportamiento posible

De las cuatro piezas, Msg es la que la mayoría infravalora, y es la más profunda. Estamos acostumbrados a pensar el estado como el corazón de una aplicación, pero en TEA el corazón conceptual es el catálogo de mensajes, porque define el universo entero de lo que a tu programa le puede pasar. Un Model te dice en qué situación estás; el tipo Msg te dice qué situaciones son siquiera concebibles. Y como es un tipo suma cerrado —no un string libre, no un objeto abierto—, ese universo es finito, enumerable y verificable por el compilador. Ahí ocurre la magia silenciosa: el case exhaustivo de update convierte la lista de mensajes en un contrato que no puedes incumplir sin que el programa se niegue a compilar. Añade una variante y todos los update del sistema te exigen decir qué haces con ella; elimina una y el compilador te lleva a cada sitio que la usaba. La documentación no puede mentir porque la documentación es el tipo. Compáralo con el action.type de texto de Flux o Redux, donde una falta de ortografía en "INCRMENTAR" produce un mensaje que nadie maneja y ningún compilador reclama: ahí el catálogo de eventos es una convención esperanzada; en Elm es una ley comprobada. Esta es la razón por la que quien viene de Elm mira los string de acción de Redux con desconfianza, y por la que TypeScript pasó años persiguiendo, con uniones discriminadas, la seguridad que Elm tenía de fábrica. La lección para tu propio diseño, uses el lenguaje que uses, es esta: modela el conjunto de eventos posibles como un tipo cerrado antes de escribir una sola línea de lógica, porque ese tipo no es andamiaje, es la especificación de tu sistema, y todo lo demás —el estado, la transición, la vista— no es más que la respuesta obligada a la pregunta que el tipo Msg plantea.

⚔️ Ensambla y estira las cuatro piezas
  1. Escribe desde cero el contador en Elm con las cuatro piezas y Browser.sandbox. Comprueba que compila y funciona sin una sola línea de más.
  2. Añade una variante Duplicar al tipo Msg pero no toques update. Lee el error del compilador y explica por qué es, en realidad, una ayuda y no un estorbo.
  3. Convierte el Model de un solo Int a un record con tres campos. Observa que update sigue devolviendo un Model nuevo con { model | ... } sin mutar el anterior.
  4. Escribe la firma de view sin implementarla y explica qué prohíbe exactamente el tipo Html Msg. ¿Por qué la vista no puede llamar a update ni ejecutar un efecto?
  5. Compara la definición de Msg con un manejador de acciones de Redux basado en string. Nombra qué error concreto atrapa el compilador de Elm que en Redux solo se descubriría en ejecución.