wandres.dev
PORTS E INTEROP · hablar con JavaScript

Flags: la configuración inicial que entrega el anfitrión

Hay datos que la aplicación necesita antes de existir: la dirección de la interfaz remota, el idioma del usuario, un identificador de sesión, el estado que quedó guardado en la visita anterior. Ninguno puede llegar por un port de entrada, porque en ese instante todavía no hay modelo, no hay subscriptions y no hay nada que escuche. Para eso están los flags: un valor que el anfitrión entrega al inicializar y que el runtime pasa a init antes de construir la primera versión del modelo. Esta lección desarrolla cómo cambia la firma de init al pasar de Browser.element a su variante con flags, por qué conviene declararlos sobre Json.Decode.Value en lugar de un record concreto, qué significa exactamente que un flag mal formado impida el arranque de la aplicación, y cómo se modela un arranque degradado que no dependa de que el exterior cumpla su promesa.

⏱ 17 min

Toda aplicación empieza con una pregunta que ella sola no puede responder: en qué circunstancias estoy arrancando. La dirección del servidor cambia entre desarrollo y producción, el idioma depende del navegador, la sesión depende de una cookie que Elm no puede leer, y el estado de la visita anterior vive en un almacenamiento al que solo el anfitrión tiene acceso. Nada de eso puede llegar por un port de entrada, y la razón es de orden temporal antes que de diseño: los ports de entrada producen suscripciones, las suscripciones se derivan del modelo, y en el instante del arranque el modelo todavía no existe. Los flags resuelven ese problema de arranque con la única solución compatible con la arquitectura: convertir la configuración en un argumento de init, entregado por el anfitrión en el mismo acto de inicializar la aplicación.

🎯 Al terminar esta lección sabrás
  • Distinguir la firma de init con flags de la que no los tiene y entender por qué el cambio afecta al programa entero.
  • Entregar flags desde el anfitrión al inicializar y describir en qué momento exacto los recibe la aplicación.
  • Declarar los flags sobre Json.Decode.Value y decodificarlos dentro de init para controlar el fallo.
  • Diseñar un arranque degradado en lugar de una aplicación que no arranca cuando el exterior incumple.

init cambia de firma y el programa cambia con él

Un programa sin configuración externa declara su función de arranque con un parámetro vacío, que representa la ausencia de datos de entrada. Al pasar a la variante con flags, ese parámetro pasa a tener el tipo de lo que el anfitrión promete entregar, y esa promesa aparece además en el tipo del programa completo. La consecuencia es que la configuración deja de ser una variable global consultada desde cualquier parte y se convierte en un argumento explícito que solo init recibe y que, a partir de ahí, viaja dentro del modelo si hace falta conservarlo.

-- Sin configuración externa: el parámetro es la unidad
main : Program () Model Msg
main =
    Browser.element
        { init = \_ -> ( modeloInicial, Cmd.none )
        , update = update
        , view = view
        , subscriptions = subscriptions
        }


-- Con flags: el tipo del programa declara qué espera de fuera
main : Program Json.Decode.Value Model Msg
main =
    Browser.element
        { init = init
        , update = update
        , view = view
        , subscriptions = subscriptions
        }

Del lado del anfitrión, la entrega ocurre en la misma llamada que monta la aplicación. El objeto que se pasa como configuración inicial se convierte en el argumento que recibe init, y esa conversión sucede antes de que exista la primera versión del modelo, antes del primer renderizado y antes de que se evalúe subscriptions por primera vez.

const guardado = localStorage.getItem("estado");

Elm.Main.init({
  node: document.getElementById("raiz"),
  flags: {
    api: "https://api.ejemplo.test",
    idioma: navigator.language,
    estadoPrevio: guardado ? JSON.parse(guardado) : null,
    ahora: Date.now(),
  },
});
🚩

Antes del modelo

Los flags llegan cuando aún no hay estado, ni vista, ni suscripciones. Es el único momento en que la aplicación puede recibir algo sin tener dónde guardarlo todavía.

📐

Aparecen en el tipo

El tipo del programa declara qué espera del anfitrión. La dependencia con el exterior deja de ser implícita y pasa a estar escrita en la fachada.

1️⃣

Una sola vez

No hay segunda entrega. Todo lo que cambie después de arrancar corresponde a un port de entrada, no a un flag.

💥

Fallan ruidosamente

Si declaras un tipo concreto y el anfitrión no lo respeta, la aplicación no arranca. Es el único fallo de frontera que Elm convierte en visible de inmediato.

Value en lugar de un record concreto

Elm permite declarar los flags con un tipo concreto, por ejemplo un record con los campos que esperas. Cuando lo haces, el runtime intenta convertir lo que el anfitrión entregó a ese tipo y, si no encaja, aborta el arranque con un error en la consola del navegador. La aplicación no se monta y el usuario ve una página vacía. Ese comportamiento es honesto pero brutal, y en producción rara vez es lo que quieres: un campo opcional ausente, una versión antigua del estado guardado o un valor nulo donde esperabas un número bastan para dejar la pantalla en blanco.

⚠️
El fallo de flags ocurre fuera de tu código y no puedes capturarlo

Cuando declaras flags con un tipo concreto, la comprobación la hace el runtime antes de llamar a init. No hay ninguna rama de tu programa que se ejecute en ese caso, porque tu programa todavía no ha empezado. No existe un manejador al que recurrir ni un estado de error que mostrar: solo un mensaje en la consola. Si quieres que el fallo sea tuyo y no del runtime, la única vía es declarar los flags como Json.Decode.Value y hacer tú la conversión dentro de init, donde ya estás en territorio propio y puedes decidir qué modelo construir.

type alias Config =
    { api : String, idioma : String, ahora : Int }


decodificador : Json.Decode.Decoder Config
decodificador =
    Json.Decode.map3 Config
        (Json.Decode.field "api" Json.Decode.string)
        (Json.Decode.field "idioma" Json.Decode.string)
        (Json.Decode.field "ahora" Json.Decode.int)


init : Json.Decode.Value -> ( Model, Cmd Msg )
init crudo =
    case Json.Decode.decodeValue decodificador crudo of
        Ok config ->
            ( arranqueNormal config, cargarDatos config.api )

        Err error ->
            -- Arranque degradado: la aplicación existe y explica qué pasó
            ( arranqueDegradado (Json.Decode.errorToString error), Cmd.none )

La diferencia entre las dos ramas no es cosmética. En la primera, la aplicación arranca con una configuración verificada y el resto del programa puede confiar en ella sin volver a preguntarse por su forma. En la segunda, la aplicación existe igualmente, muestra un estado que explica el problema y conserva la capacidad de reaccionar. Un usuario que ve un mensaje claro está en una situación radicalmente mejor que uno que ve una página en blanco, y un desarrollador que recibe un informe con ese mensaje diagnostica en minutos lo que de otro modo llevaría horas.

flowchart TD
A[anfitrion prepara los flags] --> B[Elm init con flags]
B --> C[runtime entrega el Value a init]
C --> D{decodifica bien}
D -->|Ok| E[Model completo y Cmd inicial]
D -->|Err| F[Model degradado que explica el fallo]
E --> G[primera vista]
F --> G
style C fill:#f9e2af,color:#11111b
style E fill:#a6e3a1,color:#11111b
style F fill:#fab387,color:#11111b

Qué merece ser flag y qué merece ser port

La regla que ordena la decisión es temporal y no admite mucha discusión: si el dato debe estar disponible para construir la primera versión del modelo, es un flag; si puede llegar después, es un port de entrada. Los identificadores de sesión, la dirección de la interfaz remota, el idioma detectado, las banderas de funcionalidad y el estado restaurado de la visita anterior pertenecen al primer grupo. Los mensajes de un websocket, los cambios de conectividad, las notificaciones del sistema y las respuestas a operaciones que la aplicación inició pertenecen al segundo.

Una prueba práctica ayuda a resolver los casos límite: pregúntate qué debería ocurrir si ese dato cambiara mientras la aplicación está en marcha. Si la respuesta es que nada, porque el valor pertenece al contexto fijo de esta ejecución, es un flag. Si la respuesta es que la interfaz debería reaccionar, entonces el dato no es configuración sino estado del mundo, y la configuración inicial solo puede aportar su primera fotografía.

Hay un caso que confunde con frecuencia y merece nombrarse: la hora actual. Elm no puede leer el reloj desde una función pura, así que el instante inicial suele entregarse como flag, y a partir de ahí el paso del tiempo se obtiene con suscripciones o con comandos. Confundir los dos mecanismos produce un patrón frágil que se ve a menudo: entregar por flags algo que cambia, guardarlo en el modelo y olvidar que envejece.

💡
Un flag es una fotografía, no una ventana

Lo que entra por flags es el valor que tenía el mundo en el momento del arranque, congelado para siempre. Si guardas en el modelo la anchura de la ventana entregada por flags, tendrás la anchura que había al cargar la página y no la actual. Si guardas el instante inicial, tendrás un reloj parado. La regla mnemotécnica es útil: los flags responden a cómo estaba el mundo cuando empecé, y las suscripciones responden a cómo está el mundo ahora. Todo dato que pueda cambiar necesita las dos cosas: un flag para el valor inicial y una suscripción para mantenerlo vivo.

Los flags convierten la configuración en un argumento y con ello en algo que se puede razonar

Merece la pena mirar los flags desde una perspectiva más amplia que la del mecanismo, porque resuelven un problema que casi todos los entornos resuelven mal. En un programa típico del navegador, la configuración se lee de una variable global, de un atributo del documento, de una constante inyectada en tiempo de construcción o de una llamada al almacenamiento hecha desde cualquier módulo que la necesite. El resultado es que la dependencia con el exterior queda repartida por todo el código y no aparece en ninguna firma: una función que parece pura consulta por dentro el idioma del navegador, y otra que parece autónoma depende en secreto de una variable que alguien definió al cargar la página. Nadie puede saber, leyendo un módulo, de qué depende realmente. Los flags no permiten esa dispersión, y no porque prohíban nada sino porque no hay ningún otro sitio donde poner la configuración. Al ser un argumento de init, y solo de init, la dependencia queda concentrada en un punto, escrita en el tipo del programa y obligada a viajar explícitamente hacia donde se necesite, normalmente dentro del modelo. Eso tiene tres consecuencias que se descubren con el tiempo. La primera es que se puede leer la fachada de una aplicación y saber en una línea qué necesita del mundo para existir. La segunda es que probar el programa con otra configuración es pasar otro argumento, sin instrumentar globales ni simular el entorno. La tercera, la más profunda, es que la configuración deja de ser ambiente y pasa a ser dato: algo que se decodifica, se valida, se transforma y se guarda como cualquier otro valor del dominio, sujeto a las mismas reglas y a las mismas garantías. Un sistema donde el entorno es un argumento se comporta de manera distinta a uno donde el entorno es una atmósfera, y la diferencia se nota precisamente el día en que algo del entorno cambia.

⚔️ Arranca con el mundo como argumento
  1. Convierte un programa sin flags en uno con flags y observa cómo cambian a la vez la firma de init y el tipo del programa.
  2. Entrega desde el anfitrión un objeto con cuatro campos y decodifícalo dentro de init sobre Json.Decode.Value.
  3. Rompe deliberadamente un campo de los flags con un tipo concreto declarado y comprueba que la aplicación no llega a montarse.
  4. Rehaz el mismo experimento con Value y un arranque degradado, y compara qué ve el usuario en cada caso.
  5. Clasifica seis datos de una aplicación tuya entre flags y ports de entrada, justificando cada decisión con el criterio temporal.
  6. Encuentra un dato que hayas tratado como flag y que en realidad envejece, y diseña la suscripción que debería acompañarlo.