« Elm: Proyecto Juego « || Inicio || » ¿Se puede liberar la … »

Elm: Creación rápida de aplicaciones. StartApp y Mailboxes

Última modificación: 25 de Noviembre de 2016, y ha tenido 227 vistas

Etiquetas utilizadas: || ||

(Esta entrada está inspirada en esta otra)

Una de las grandes bondades que ofrece Elm es lo natural y simple que resulta crear la arquitectura de una aplicación siguiendo los paradigmas funcionales. Una aplicación en Elm es, esencialmente, una cadena reactiva de acciones que sirven de transición entre diversos estados de un modelo prefijado y cuyos cambios producen una renderización de la vista cuando se detecta algún cambio (en forma de señales). Gracias al paradigma funcional, no tienes que hacer uso de mutaciones, simplemente creas un nuevo estado y la aplicación entera se adapta automáticamente.

Para mostrar lo sencillo que es este proceso vamos a generar una aplicación sencilla pero completa. Primero lo haremos usando la librería StartApp, que proporciona las herramientas necesarias para generar todo el ciclo de vida de la aplicación, y después veremos cómo podemos conseguir lo mismo sin hacer uso de esta librería, creando las funciones y estructuras de datos necesarias para comunicar las distintas partes del ciclo.

Usando StartApp

Esta librería permite abstraer todos los mecanismos básicos para construir una aplicación Elm. Está diseñada para poner en funcionamiento una aplicación completa sin necesidad de conocer todos los detalles que hacen que todas las partes de la aplicación encajen entre sí. Para ello, basándonos en el paradigma Modelo-Vista-Controlador (MVC) que sigue la librería, hemos de proporcionar a StartApp:

  1. el modelo de datos que almacenará el estado de la aplicación (model)
  2. una vista que proporcione una representación visual del estado actual (view), y
  3. un método que, a través de señales específicas generadas dentro o fuera de la aplicación, genera la transición de un estado de la aplicación al siguiente (update).

La aplicación que usaremos como ejemplo es muy simple, contendrá poco más que lo mínimo para demostrar cómo funciona. Aprovecharemos, además, para presentar de forma muy introductoria la librería Html que proporciona Elm, y así matamos dos pájaros de un tiro.

Nuestra aplicación mantendrá una lista de códigos que puede ser modificada por el usuario por medio de dos botones. Con el primer botón, "Añade Código", el usuario puede agregar un nuevo código a la lista (que se refleja por medio de una lista de enteros consecutivos); con el segundo botón, "Reinicia", el usuario puede limpiar toda la lista y volver a empezar.

Como siempre, comenzamos importando las librerías que necesitamos (para no complicar la notación, las importamos exponiendo todo su contenido de forma abierta, aunque debemos recordar que no es lo más recomendable):

import Html exposing (..)
import Html.Events exposing (..)
import Html.Attributes exposing (..)
import Signal exposing (..)
import StartApp.Simple as StartApp

Vamos a empezar mostrando la estructura que tiene la función main que sirve de director general de la aplicación que vamos a montar. Como toda función main, devolverá una señal de algún tipo (Html, Element) que puede ser usada por la infraestructura de Elm para realizar los sucesivos renderizados de la aplicación.

En este caso, hemos dicho que vamos a usar la librería StartApp (en concreto, StartApp.Simple, como se ve en la importación anterior) para manejar todo el proceso, por lo que el patrón que sigue la función main es el siguiente:

main : Signal Html
main =
    StartApp.start
      { model = modeloInicial
      , view = vista
      , update = actualiza }

Como el renderizado (vista) genera finalmente un Html, main devolverá una señal de este tipo a medida que los eventos se van sucediendo y el programa actualiza el estado del modelo. Podemos observar cómo esta librería expone claramente las tres partes del paradigma MVC tal y como lo habíamos presentado en los requerimientos iniciales.

Vayamos parte a parte analizando cada uno de estos tres componentes.

Modelo

El modelo debe definir el estado completo de la aplicación. En este caso, como la aplicación es de juguete, será un estado muy simple que contendrá únicamente una lista de enteros representando los códigos que la aplicación almacena.

type alias Modelo =
    { codigos: List Int
    }

Además, nos hace falta definir un estado inicial con el que dará inicio la aplicación (y que se usará también para resetearla).

modeloInicial : Modelo
modeloInicial =
    { codigos = [] 
    }

Actualiza

Ya tenemos el modelo de datos que es capaz de almacenar los estados por los que pasará la aplicación, ahora necesitamos establecer de qué forma se realizarán los cambios en el estado, algo que vendrá determinado por las acciones se puedan realizar sobre el modelo. Para ello definiremos un tipo unión que contendrá las posibles acciones que pueden llevarse a cabo durante la ejecución de la aplicación.

En este caso, hemos comentado que solo se permiten 2 acciones: Nuevo, que añade un nuevo código a la lista de códigos del estado, y Reinicia, que elimina todos los códigos y reinicia la aplicación.

type Accion = Nuevo
    | Reinicia

Finalmente, la función actualiza que genera nuevos estados a partir de la acción a ejecutar y el estado actual, será:

actualiza : Accion -> Modelo -> Modelo
actualiza accion estadoActual =
    case accion of
      Nuevo    ->
        { estadoActual | codigos = estadoActual.codigos ++ [List.length estadoActual.codigos] }
      Reinicia ->
        modeloInicial

Esta función es simplemente un gran case en el que, recibiendo una acción y el estado actual del modelo crea y devuelve, en función de la acción, un nuevo modelo que modifica adecuadamente el de entrada para adpatarse a los cambios que deben producir las distintas acciones contempladas. Observa el uso de registros para mantener y modificar los estados.

Vista

Tras haber definido el estado de la aplicación, y cómo puede ser modificado, hemos de dar la función que se encargará de proporcionar la representación visual de un estado instantáneo concreto:

vista : Address Accion -> Modelo -> Html
vista address modelo = -- recibe la dirección donde se envían los mensajes al mailbox
    -- y el modelo actual
    div []
        [ 
        -- Botón de añadir código. Lanza el mensaje "Nuevo" al actualizador
        button [ onClick address Nuevo ] 
               [ text "Añade Código" ],
        -- Botón de reniciar. Lanza el mensaje "Reinicia" al actualizador
        button [ onClick address Reinicia ] 
               [ text "Reinicia" ],
        -- Mostramos el número de códigos en la lista
        h2     [] 
               [ text (modelo.codigos |> List.length |> numCodigos) ],
        -- Mostramos los códigos
        div    [] 
               (List.map (\t -> text (toString t ++ " ")) modelo.codigos)
        ]

Una aplicación más compleja requerirá definir funciones auxiliares que se encarguen de los detalles de cada una de las secciones que se van renderizando, pero en este caso podemos escribir todo dentro de la misma función ya que estamos llevando al mínimo los requisitos de la aplicación para centrarnos en entender la estructura general.

Como muestra, hacemos uso de una función que decora el número de códigos en la lista:

numCodigos : Int -> String
numCodigos ncod = case nev of
  0 -> "No Hay Códigos"
  1 -> "1 Código en la lista"
  _ -> (toString ncod) ++ " Códigos en la lista"

Vamos a analizar cómo funciona vista, pero recuerda que es la función StartApp.start la que se encargará de todo el flujo de información entre los componentes, por lo que la forma en que está definida esta función se debe a las particularidades de funcionamiento de esta librería. En cualquier caso, la forma que sigue es bastante natural, y la reutilizaremos directamente cuando hagamos la versión de nuestra aplicación sin hacer uso de la librería StartApp.

Esta vista permite interactuar con el usuario (en este caso, por medio de dos botones) y debe conectarse con la función actualiza que vimos antes, y que se encarga de actualizar el estado del modelo en función de esta interacción. Por ello, la función vista recibe una dirección donde mandar los mensajes con las acciones (que finalmente le llegarán a la función actualiza gracias al funcionamiento de StartApp), y el estado actual del modelo para mostrar los datos actuales.

El contenido de la función que hemos definido tiene el fin de manejar diversos objetos del DOM para dar una representación Html. En general, la mayoría de los objetos Html representables reciben 2 argumentos:

objeto [ atributos ] [ contenido ]

Toda nuestra representación va dentro de un div que no tiene atributos, y cuyo contenido está formado por diversos objetos. Como h2 y div son habituales en Html, y su uso es relativamente directo en Elm, nos centramos en los botones, button, que ofrecen una forma de manejar eventos (en este caso, recibir Clics), y se encargarán de enviar las acciones a actualiza por medio del sistema de comunicación que establece StartApp.

Los últimos h2 y div muestran, respectivamente, el número de códigos que tiene el estado, y el contenido de éstos. Observa el uso de text, para convertir cadenas en texto representable en Html.

Vamos a detenernos un poco más en las acciones y las direcciones de envío de mensajes con el fin de comprenderlas adecuadamente, ya que son el núcleo de la conectividad entre las diversas componentes de la aplicación.

La librería StartApp crea un buzón (mailbox) interno, con una dirección única, donde puedes enviar aquello que quieras que sea procesado por la función de actualización. Como puedes ver por la signatura de la función vista, la dirección es de tipo Address Accion. Como vimos anteriormente, el tipo Accion que definimos abstrae las dos operaciones que se pueden realizar desde la aplicación, y lo que hemos hecho con los botones es hacer que, al recibir el evento Clic, envíen la acción correspondiente al buzón de entrada que StartApp ha creado para nosotros.

La librería StartApp es la encargada de mantener este ciclo de acción -> modificación -> representación -> ... permanentemente activo y conectando las componentes siguiendo las normas que le hemos dado.

Con el fin de que quede claro el proceso, resumimos a continuación lo que hemos hecho:

  1. La función main comienza nuestra aplicación usando StartApp.start, a la que le pasamos los 3 componentes principales del ciclo de ejecución: modelo, vista y atualiza.
  2. El modelo representa el estado instantáneo de la aplicación completa.
  3. La vista proporciona una representación visual del estado actual, y también comunica las acciones que realiza el usuario y que tienen como efecto actualizar el modelo de la aplicación.
  4. La función de actualización toma las acciones que recibe de la vista y crea un nuevo estado a partir del estado actual.
  5. La librería StartApp se encarga de disparar de nuevo la renderización de la vista y el ciclo se repite.

Además de simple, esta aproximación evita trabajar con elementos mutables, que dan muchos problemas en la verificación y estabilidad de aplicaciones web.

Viviendo sin StartApp

Una vez que entendemos qué hace StartApp, el siguiente paso es vivir sin él... al menos ser capaces de hacer lo mismo que hace esta librería para cuando sea necesario no usarla, ya que StartApp es una librería que proporciona la comunicación necesaria entre las componentes en casos básicos, pero que se queda corta cuando hemos de lidiar con situaciones algo más elaboradas (por ejemplo, con otro tipo de señales o con ports de comunicación con Javascript). Por ello, vamos a ver ahora qué necesitaríamos cambiar/añadir en nuestra aplicación para conseguir los mismos resultados pero sin hacer uso de esta librería.

El buzón

La librería StartApp trabaja con un envío de acciones a un buzón que nosotros no hemos creado explícitamente, sino que lo proporciona la propia librería. Este buzón (Mailbox) es un concepto que proporciona de manera independiente Elm y que genera identificadores, direcciones, en los que se pueden recibir mensajes, y que emiten una señal con un valor específico cuando llega algún mensaje nuevo a esa dirección. De esta forma, en nuestro ejemplo, cuando mandamos una acción (que es el mensaje) a la dirección del buzón por medio del manejador onClick en nuestra vista, se dispara un aviso de actualización del estado del modelo.

Para crear nuestro propio buzón de acciones podemos hacer:

acciones: Mailbox Accion
acciones =
    mailbox Reinicia

El buzón de acciones tiene como tipo Mailbox Accion, y cuando lo creamos (es solo un repositorio) hemos de darle un ejemplo del tipo de Accion que comunicará (en este caso, Reinicia). Por ello, es común que en las acciones se añada una acción extra, por ejemplo NoOp (No operativa), que solo sirve para dar ese ejemplo inicial, pero que no refleja ninguna acción real. Sea de una forma u otra, lo importante es crearlo con algún valor adecuado concreto.

Modelo de estado reactivo

El siguiente paso es crear el procedimiento que debe seguir Elm para tomar el estado inicial y convertirlo en un modelo reactivo de forma que, cuando se recibe un mensaje de acción, el método de actualización calcule el estado siquiente (creando uno nuevo). En este sentido, la programación funcional hace que esta tarea sea sencilla, ya que permite ver el modelo reactivo simplemente como un plegado sobre la señal de acciones (por tanto, un foldp) que nuestro buzón envía, usando como inicio del plegado el modelo inicial.

modelo: Signal Modelo
modelo =
    foldp actualiza modeloInicial acciones.signal

Observa que, como el modelo varía en el tiempo, lo definimos explícitamente como una señal de Modelo.

Aplicando Main

Finalmente, necesitamos reemplazar el método main para hacer nosotros mismos las conexiones adecuadas entre las distintas componentes que hemos definido:

main : Signal Html
main =
    Signal.map (vista acciones.address) modelo

Para ello, main aplica la función vista sobre las señales Modelo que genera la actualización continua de la aplicación. Como la vista recibe tanto la dirección donde enviar las acciones como el modelo, pero la dirección no es una señal, sino un valor fijo establecido por el buzón, es conveniente tratarlo como una aplicación parcial (vista acciones.address) que toma la dirección del buzón que hemos definido antes y devuelve una función que recibe estados (modelos) de la aplicación. De esta forma, cuando el modelo se actualiza (es una señal) main provocará la llamada de la función vista con el nuevo estado, produciendo una señal Html que Elm se encargará de renderizar actualizada en el navegador (y además de una forma extremadamente rápida, puedes ver una comparativa de rendimientos de renderización aquí).

El siguiente diagrama resume nuestro nuevo panorama sin StartApp:

Puedes comprobar que el resultado obtenido es exactamente el mismo que se obtenía por medio de la librería StartApp.

Cosas que puedes probar

Para terminar de entender bien esta entrada, te propongo que adaptes las aplcaciones que hemos visto o propuesto en las entradas de Elm anteriores para hacer uso de la llibrería StartApp. Después, puedes volver a reescribirlas haciendo uso de mailboxes de forma explícita, pero implementando tú el sistema de llamadas y evitandoel uso de StartApp, tal y como hemos mostrado en el ejemplo aquí usado.

« Elm: Proyecto Juego « || Inicio || » ¿Se puede liberar la … »