Los cuatro programas de Elm: sandbox, element, document y application
Casi todos los frameworks de interfaz ofrecen un único punto de entrada y dejan que el programador decida después cuánta responsabilidad asume sobre el documento, la URL y el ciclo de vida de la página. Elm hace lo contrario: publica cuatro constructores de programa distintos, cada uno con una firma diferente, y esa firma es a la vez una declaración de capacidades y una declaración de renuncias. Esta lección recorre la escalera completa —Browser.sandbox, Browser.element, Browser.document y Browser.application— mostrando que no forman un catálogo de sabores intercambiables sino una progresión estricta en la que cada peldaño concede un poder nuevo y cobra por él una obligación nueva. Se examina qué desaparece de la firma cuando un programa no puede producir efectos, por qué el paso de element a document consiste en cambiar el tipo de retorno de la vista, qué implica que application reciba una Key y dos manejadores de navegación, y cómo se lee esa firma para decidir qué programa necesita un proyecto antes de escribir la primera línea. El criterio que se destila es que el programa correcto es el mínimo que permite expresar lo que la aplicación realmente hace, porque cada capacidad no usada es superficie de error regalada.
Un lenguaje revela sus convicciones en los sitios donde podría haber ofrecido una sola cosa y ofrece cuatro. Elm podría haber publicado un único constructor de programa, generoso y configurable, con campos opcionales para quien quiera controlar el título del documento o interceptar la navegación, y con valores por defecto sensatos para quien no. Sería lo habitual, y sería más cómodo el primer día. En lugar de eso publica Browser.sandbox, Browser.element, Browser.document y Browser.application, cuatro funciones con cuatro firmas incompatibles entre sí, y obliga a elegir una antes de escribir nada. La elección no es estética. Cada firma describe con precisión qué puede hacer el programa y, sobre todo, qué no puede hacer: un sandbox no puede producir un efecto ni aunque quiera, porque en su tipo no hay ningún sitio donde un efecto quepa. Lo que parece una molestia burocrática es en realidad la aplicación más pura de la tesis del lenguaje al nivel del propio punto de entrada: las capacidades se declaran, no se descubren, y el compilador se encarga de que la declaración sea cierta.
- Leer las cuatro firmas de
Browsercomo declaraciones de capacidad y distinguir qué concede y qué exige cada peldaño. - Explicar por qué un
sandboxno puede producir efectos y por qué esa imposibilidad es una propiedad del tipo, no una convención. - Identificar el punto exacto en que un programa deja de ser un componente incrustado y pasa a ser dueño del documento.
- Elegir el constructor mínimo que expresa lo que un proyecto necesita y planificar la migración cuando ese mínimo cambia.
Una escalera de renuncias
Las cuatro funciones se leen mejor de abajo arriba, porque cada una añade exactamente un elemento a la anterior. Browser.sandbox recibe un registro con tres campos y nada más: un modelo inicial, una función de actualización que va de mensaje y modelo a modelo, y una vista. No hay Cmd, no hay Sub, no hay banderas de inicio. La ausencia es literal: no existe ninguna posición en la firma donde un efecto pueda aparecer, así que un programa construido con sandbox es demostrablemente incapaz de hablar con el mundo. Browser.element restituye ese canal: el modelo inicial pasa a ser una tupla de modelo y comando, update devuelve también una tupla, aparece subscriptions y aparecen las banderas que el anfitrión inyecta al arrancar. Sigue devolviendo Html msg, y esa es la clave de su papel: es un nodo que vive dentro de una página que no controla.
Browser.sandbox :
{ init : model
, update : msg -> model -> model
, view : model -> Html msg
}
-> Program () model msg
Browser.element :
{ init : flags -> ( model, Cmd msg )
, update : msg -> model -> ( model, Cmd msg )
, subscriptions : model -> Sub msg
, view : model -> Html msg
}
-> Program flags model msg
El tipo de retorno merece una lectura aparte porque es común a los cuatro. Program flags model msg no es un valor que se ejecute ni un objeto con métodos: es la descripción completa de una aplicación, con los tres tipos que la caracterizan a la vista en su propia firma. El primer parámetro dice qué datos acepta del anfitrión al arrancar, y en sandbox está fijado al tipo unitario precisamente porque no acepta ninguno. Los otros dos dicen cuál es el estado y cuál el vocabulario de mensajes. Una aplicación entera queda así resumida en una línea que se puede leer sin abrir ningún fichero, y esa línea es lo único que el anfitrión necesita conocer.
-- Un sandbox: ni banderas, ni efectos, ni titulo, ni URL
main : Program () Model Msg
main =
Browser.sandbox { init = inicial, update = update, view = view }
-- Un element: acepta banderas y habla con el mundo
main : Program Config Model Msg
main =
Browser.element
{ init = init
, update = update
, subscriptions = subscriptions
, view = view
}
sandbox
Puro por construcción: sin Cmd, sin Sub, sin banderas. Ideal para enseñar el bucle y para ejercicios donde el efecto distraería del mecanismo.
element
Un nodo dentro de una página ajena. Ya puede pedir datos y escuchar el mundo, pero convive con otro código que manda en el documento.
document
Devuelve título y cuerpo. El programa es la página entera y el título deja de ser un efecto externo para ser parte de la vista.
application
Añade la URL. Recibe una Key, intercepta los clics en enlaces y reacciona a los cambios de dirección. Es el peldaño de la aplicación de una sola página.
Dónde deja de ser un componente y empieza a ser la página
El salto de element a document consiste en una sola cosa, y por eso conviene mirarlo despacio: cambia el tipo de retorno de la vista. Donde element devuelve Html msg, document devuelve un Document msg, que es un registro con un campo de título y una lista de nodos para el cuerpo. La consecuencia es más profunda de lo que sugiere el tamaño del cambio. En cualquier otro entorno, poner el título de la pestaña es una mutación imperativa sobre un objeto global, ejecutada en un efecto secundario y por tanto invisible para el sistema de tipos. Al convertirlo en un campo del valor que devuelve la vista, el título pasa a ser una función pura del modelo, se recalcula con la misma disciplina que el resto de la interfaz y desaparece la clase entera de fallos en la que la pestaña dice una cosa y la pantalla dice otra.
Browser.document :
{ init : flags -> ( model, Cmd msg )
, update : msg -> model -> ( model, Cmd msg )
, subscriptions : model -> Sub msg
, view : model -> Document msg
}
-> Program flags model msg
type alias Document msg =
{ title : String
, body : List (Html msg)
}
Merece la pena detenerse en lo que se gana. Cuando el título es una mutación, hay que acordarse de emitirla en cada transición que lo cambie, y basta olvidar una para que el estado visible se desincronice sin que nada lo señale. Cuando el título es un campo de la vista, no hay nada que recordar: si el modelo cambia, la vista se recalcula entera y el título viaja con ella. El patrón es el mismo que ya has visto con la interfaz —describir en lugar de ordenar— aplicado a un rincón que casi todos los frameworks dejan fuera de su modelo declarativo.
El último peldaño añade la URL. Browser.application conserva todo lo de document y le suma tres cosas: init recibe además la URL con la que arrancó la página y una Key de navegación, y el registro incorpora onUrlRequest y onUrlChange, dos funciones que convierten los eventos de navegación en mensajes del vocabulario propio. Esa Key es un valor opaco que solo puede obtenerse dentro de un application, y su existencia es la garantía estructural de que nadie puede alterar el historial del navegador desde un programa que no se ha declarado responsable de la navegación. La firma no describe una funcionalidad opcional: describe un contrato.
Browser.application :
{ init : flags -> Url -> Key -> ( model, Cmd msg )
, update : msg -> model -> ( model, Cmd msg )
, subscriptions : model -> Sub msg
, view : model -> Document msg
, onUrlRequest : UrlRequest -> msg
, onUrlChange : Url -> msg
}
-> Program flags model msg
Elegir el mínimo y migrar cuando deje de bastar
La regla práctica es escoger el constructor más bajo que permita expresar lo que la aplicación hace de verdad, no el más alto por si acaso. Un ejercicio didáctico o un componente de estado puramente local va en sandbox, y la imposibilidad de pedir datos deja de ser una limitación para ser una forma de mantener la atención donde debe estar. Un fragmento interactivo que se incrusta en una página existente —un buscador dentro de un sitio hecho con otra tecnología, un panel dentro de una aplicación heredada— va en element, porque el resto de la página tiene dueño. Una aplicación completa sin rutas internas va en document. Una aplicación de una sola página con varias vistas direccionables va en application, y solo entonces aparecen la Key, el enrutador y los mensajes de navegación.
Subir un peldaño es una operación mecánica, y conviene saberlo para no elegir de más por miedo a rehacer trabajo. De sandbox a element se envuelven las salidas de init y update en tuplas con Cmd.none y se añade un subscriptions que devuelve Sub.none. De element a document se envuelve la vista en un registro con título y cuerpo. De document a application se añaden dos constructores de mensaje y se guarda la Key en el modelo. En los cuatro casos, Model, Msg, la lógica de update y el árbol de la vista permanecen intactos, porque la arquitectura es la misma y lo único que cambia es el contrato con el anfitrión.
flowchart LR S[sandbox sin efectos] --> E[element con Cmd y Sub] E --> D[document con titulo y cuerpo] D --> A[application con Url y Key] E --> P[Vive dentro de otra pagina] D --> Q[Es la pagina entera] A --> R[Controla la navegacion] style S fill:#a6e3a1,color:#11111b style D fill:#89b4fa,color:#11111b style A fill:#cba6f7,color:#11111b
Hay una asimetría que la mayoría de los entornos nunca corrige: dedicamos un enorme esfuerzo a tipar el interior de nuestros programas y dejamos el borde exterior —el arranque, la relación con el anfitrión, el acceso a los recursos globales— gobernado por convenciones, documentación y disciplina. El objeto del documento está ahí para quien lo quiera tocar; el historial del navegador está ahí; el título de la pestaña está ahí. Cualquier función, a cualquier profundidad de la pila de llamadas, puede alcanzarlos, y ninguna firma lo delata. El resultado es que la pregunta más elemental sobre un programa —qué puede hacerle este código a mi página— no tiene respuesta salvo leyéndolo entero. Los cuatro constructores de Elm son la respuesta a esa asimetría, y su lección excede con mucho al lenguaje. Al partir el punto de entrada en cuatro tipos incompatibles, la capacidad deja de ser algo que se descubre auditando y pasa a ser algo que se lee en una línea: si el programa se construyó con sandbox, no hay auditoría que hacer sobre sus efectos, porque no existe ninguna posición en su tipo donde un efecto pueda alojarse; si se construyó con element, sé que puede hablar con el mundo pero no puede tocar el título ni la URL; si aparece una Key en el modelo, sé sin buscar nada más que este programa manipula el historial. Es el mismo principio que gobierna los sistemas de capacidades en seguridad —no puedes usar lo que no te han entregado— aplicado al arranque de una aplicación de interfaz, y su virtud no es impedir el abuso de un programador malicioso sino algo mucho más cotidiano: eliminar la incertidumbre. Un tipo que puede hacer cualquier cosa no informa de nada. La renuncia deliberada a capacidades que no se necesitan no es ascetismo, es la única manera conocida de que una firma diga la verdad completa.
- Escribe de memoria las cuatro firmas y señala, en cada salto, exactamente qué campo aparece y qué obligación introduce.
- Construye un contador con
sandboxe intenta añadirle una petición al servidor; describe con precisión por qué el tipo lo impide y en qué punto falla la compilación. - Migra ese contador a
elementcon el mínimo cambio posible y comprueba queModel,Msgyupdatesobreviven sin tocarse. - Convierte el resultado en
documenthaciendo que el título de la pestaña dependa del valor del contador, y razona qué clase de desincronización acaba de volverse imposible. - Toma tres proyectos que conozcas y decide para cada uno cuál es el constructor mínimo; justifica la decisión sin apelar a lo que podría necesitar en el futuro.
- Argumenta en contra de esta lección: defiende un único constructor configurable y enumera con honestidad qué garantías se pierden al adoptarlo.