Un sorbo de TEA: introducción a Elm (I)

¿Has creado alguna aplicación con Redux o una de sus implementaciones? Si la respuesta es si entonces debes saber que ya conoces bastante acerca de Elm y su arquitectura (The Elm Architecture o TEA para abreviar) ya que Dan Abramov se inspiró en ella cuando creó Redux, aquí podrás averiguar el porqué. Si la respuesta es no entonces esta es una buena oportunidad para ver de qué va todo esto.

Elm

Logo de Elm

Pero ¿qué es Elm para empezar? En su web dicen:

A delightful language for reliable webapps

Extendiendo esta definición un poco más podríamos decir que Elm es un lenguaje de programación funcional que compila a Javascript especialmente ideado para crear aplicaciones web robustas.

  • Lenguaje de programación funcional: esto significa que todo el código que escribes es funcional por defecto, no tienes que hacer ningún esfuerzo extra. Esto es así porque el lenguaje no es multiparadigma como JS, es decir, no puedes hacer Programación Orientada a Objetos con Elm (no hay clases u objetos), solo Programación Funcional pura.
    Es importante destacar también que al diseñar el lenguaje se puso un gran énfasis en hacerlo fácil de usar y de aprender, a diferencia de otros lenguajes funcionales la curva de aprendizaje de Elm es suave, para empezar a hacer cosas solo se necesitan ganas y disponer de un rato para jugar con los ejemplos básicos.
  • Compila a Javascript: el código Elm se convierte a Javascript en un proceso de build parecido al que se hace con ES6+, Typescript, Flow, etc.. Elm dispone de un set de herramientas basadas en npm para llevar a cabo este proceso.
  • Especialmente ideado para crear webapps: Elm nace de un trabajo de investigación realizado por su autor, Evan Czaplicki, en el que desarrolla cómo debería implementarse un lenguaje de programación para crear clientes web.
  • Robusto: Gracias a su compilador y su sistema de tipos el código Elm no produce excepciones en tiempo de ejecución.
No Runtime Exceptions

Sintaxis

Antes de sumergirnos en los detalles de cómo se crea una aplicación con la arquitectura Elm es necesario conocer algo de su sintaxis y algunos conceptos básicos y la mejor forma de hacerlo es mostrando algo de código.

No vamos a entrar en los detalles del sistema de build de Elm en este artículo pero puedes jugar con el código aquí presentado utilizando Ellie, un rollo JSFiddle pero para Elm.
Este ejemplo lo puedes cargar mediante este enlace.

https://ellie-app.com/fL2TwCtD9a1/0

Cabecera del módulo

Todos los archivos *.elm son módulos (una colección de cosas Elm que se exponen al exterior) y se definen con esa cabecera.

En este caso el archivo se llamaría Main.elm, si se llamara Ejemplo.elm la cabecera sería module Ejemplo exposing (..).

No hablaremos de la porción exposing(..) porque no es importante ahora mismo pero ahí podríamos declarar exactamente lo que hacemos visible a otros módulos.

Importar otros módulos

Para usar cosas de otros módulos tienes que traerlas de forma explícita al módulo en el que estás con un import, muy parecido a lo que haríamos en ES6+.

En este caso en la línea 15 estamos usando la función text que pertenece al módulo Html por eso lo importamos.

No hablaremos de la porción exposing(..) porque no es importante ahora pero ahí podríamos declarar exactamente qué nos queremos traer, definir alias para lo que estamos importando y otras cosas.

Funciones

Las funciones se definen con un nombre, una lista de argumentos separados por espacio, un signo de igual y a continuación el cuerpo de la función.

nombreFuncion arg1 arg2 arg3 = cuerpoDeLaFuncion

Las funciones SIEMPRE retornan un valor y no hace falta decirlo de forma explicita con return.

La razón por la que esto es así es porque el único propósito de las funciones en Elm es calcular algo y devolver el resultado, para producir efectos secundarios existen otros mecanismos que veremos en otra ocasión. Y si, esto tiene una serie de implicaciones y es una de las primeras cosas a las que cuesta acostumbrarse.

Records

Los records (registros en castellano) son pares de clave-valor accesibles por notación de punto, muy parecidos a los objetos de JS pero con algunas diferencias:

  • Son inmutables (no se pueden modificar): si quieres modificar su valor tienes que pasarlo por una función que devolverá una copia nueva del record con las modificaciones aplicadas.
  • No son extendibles: Al operar con un record no puedes añadirle o quitarle miembros. Por ejemplo no podrías añadir surname al record userRecord al operar con él en una función, menos aún intentar quitarle la propiedad name ya que para empezar no existen en Elm los conceptos de null, undefined o delete. No runtime expections, ¿recuerdas? :)
  • Sus tipos son fijos: Al operar con un record no puedes cambiar el tipo de sus miembros. Por ejemplo no podrías asignar 100 a userRecord.name porque éste espera un String y 100 es un Int.

Punto de entrada

main es el único punto de entrada de todo programa y es una función que debe estar definida en el archivo principal de nuestro proyecto, que no recibe parámetros y que devuelve la representación html del DOM, en el caso que nos ocupa devuelve un nodo de texto, que es el resultado de llamar a la función text pasándole como parámetro el resultado de la llamada a la función sayHello pasándole el record userRecord.

Como se puede ver los paréntesis ayudan al compilador a entender que solo le pasamos un parámetro a la función text, no dos. 
Existen otras formas más elegantes de hacer lo mismo pero no hablaremos de ello en esta ocasión.


La arquitectura Elm

Una taza de TEA calentita

En el sitio web definen muy bien qué es la TEA:

La arquitectura Elm es un sencillo patrón sobre el que construir aplicaciones web. Facilita la modularidad, la reutilización de código y el testeo. El resultado final es que permite crear aplicaciones web complejas de forma sencilla que se mantienen bien a través del tiempo mientras se refactoriza y se añaden características.

La arquitectura separa la lógica del programa en tres bloques claramente separados:

  • Model: el estado de la aplicación.
  • Update: la función que actualiza el estado.
  • View: la función que renderiza el estado a Html.

Para ilustrar el funcionamiento básico veremos el típico ejemplo del contador (puedes jugar con el código en este proyecto Ellie):

https://ellie-app.com/kpyDm6Vzga1/4

A continuación voy a explicar las diferentes partes pero obviando lo que ya sabemos del ejemplo anterior.

Importar otros módulos (II)

Contrastando con el ejemplo anterior aquí vemos dos imports nuevos: Html.Attributes y Html.Events. Y quizás te preguntes ¿por qué los importamos si ya hemos importado Html?

Pues bien, como se puede ver en esta representación de la estructura de archivos donde se sitúan los módulos, el nombre del módulo se corresponde con el nombre de todas las carpetas desde la raíz del proyecto hasta el nombre del archivo, éste incluido.

Así funciona la nomenclatura de módulos en Elm, que es de obligado cumplimiento para que el compilador no se queje.

Modelo

El Modelo de la tríada View>Update>Model puede ser tan simple como esto o tan complejo como requiera tu aplicación.

En aplicaciones grandes los modelos de otros módulos pueden componerse para formar estructuras más complejas.

Update

Veamos las distintas responsabilidades que conforman el proceso de actualización en la arquitectura Elm:

  • Los mensajes

Dentro de Elm la generación del DOM es un efecto secundario al que no tenemos acceso alguno, el DOM queda totalmente aislado y fuera del alcance de nuestras funciones, por suerte Elm proporciona un mecanismo para que podamos comunicarnos desde el DOM a nuestro código mediante mensajes.

Los mensajes en Elm son de un tipo de dato especial llamado Union Type. Son una enumeración de varias opciones (también llamadas type constructors) identificadas con un nombre, muy parecido a los enums de otros lenguajes pero más potente.

Los Union Types se declaran con la palabra reservada type, el nombre que le queramos dar, un signo de igual y luego uno o más nombres separados por el operador pipe |.

Los mensajes que definamos luego serán utilizados en la función de Update para decidir qué camino tomamos dentro de una serie de opciones (lo vemos en el siguiente punto).

  • La función de Update

Cuando el runtime de Elm recibe un mensaje éste automáticamente hace una llamada a la función Update de la tríada View>Update>Model; en este caso la función se llama actualizar; pasándole como parámetros el mensaje recibido y el estado actual de la aplicación (el modelo). Entonces la función se ocupa de actualizar y retornar el modelo basándose en la información que le proporciona el mensaje que se le ha enviado.

Dentro de la función vemos una nueva estructura case mensaje of llamada case statement, que viene a ser algo parecido a un switch, aunque da mucho más juego.

Lo que un case statement hace básicamente es buscar entre todos sus case uno que coincida con el mensaje que se le ha pasado. Este proceso se llama pattern matching.
En el ejemplo que nos ocupa significa que al clicar los botones del contador, que son los que envían los mensajes, se ejecutará el case Incrementar cuando llegue el mensaje Incrementar y el case Decrementar cuando llegue el mensaje Decrementar.

Finalmente, dentro de los case podemos ver la sintaxis de actualización del modelo, que es visiblemente distinta a lo que estaríamos acostumbrados de JS.

Cabe remarcar que el case statement es una expresión y que resuelve a un valor (el valor actualizado del modelo), por lo tanto la función de Update devuelve un case statement (recordad que las funciones siempre devuelven), y éste a su vez devuelve el nuevo modelo.

View

Tras ejecutarse la función de Update el runtime de Elm recoge el nuevo valor del modelo y ejecuta automáticamente la función View de la tríada View>Update>Model; en nuestro caso la función vista; pasándole el nuevo modelo recibido para finalmente calcular y devolver la nueva representación del DOM al runtime para que lo pinte por pantalla.

En esta función cabe destacar la forma en que se declara la estructura del DOM. Son simples funciones!

La librería Html de Elm contiene funciones para todos los elementos y atributos de la especificación html pero antes de ver los detalles vamos a introducir dos nuevos tipos: las listas y las tuplas.

  • List: Las listas son parecidas a los Arrays de JS pero solo pueden contener elementos de un mismo tipo.
    También se declaran con corchetes miLista = [ 1, 2, 3 ].
  • Tuple: Una tupla es una enumeración fija de elementos de uno o varios tipos distintos.
    Se declaran entre paréntesis miTupla = ( 1, "hola", False ).

Ahora que conocemos las listas y las tuplas podemos entender la firma de las funciones de los elementos html:

nombreTagHtml listaAtributos listaHijos
| | |
función arg1 arg2
  • listaAtributos es una lista de funciones de atributos como style , class o onClick.
  • listaHijos es una lista con funciones de otros elementos html anidados como div, span o h1.

La lista de atributos está compuesta por funciones cada una con distintas firmas. En el caso de style por ejemplo recibe una lista de tuplas (String, String).

span [ style [ ( "padding", "5px" ), ( "color", "blue" ) ] []

En el caso de class en cambio recibe un String.

div [ class "col-sm" ] [ text "Hola" ]

Otro tipo de atributo a destacar son los que envían mensajes como el click de los botones.

button [ onClick Incrementar ] [ text "Incrementar contador" ]

Es posible parametrizar los mensajes que se envían desde del DOM de forma que transporten valores pero esto queda fuera del ámbito de este artículo.

Inicialización del programa

Para acabar solo queda conectar las tres partes View>Update>Model con el runtime de Elm y para ello se tiene que llamar a la función beginnerProgram del módulo Html pasándole nuestros model, view y update.

Existen otras funciones con más parámetros que permiten aumentar la arquitectura para que soporte parametrización desde JS o la ejecución de efectos como conexiones a sockets o peticiones Http pero esto queda para otro artículo.

Recapitulando

Hemos aprendido unas cuantas cosas:

  • Qué es Elm.
  • Sintaxis y estructura básica de un programa Elm.
  • La arquitectura Elm y sus actores / conceptos asociados.

¿Redux, reducido?

Si eres usuario de Redux te habrás dado cuenta que el flujo unidireccional de la arquitectura Elm es muy parecido al de Redux y habrás notado también el poco código que hemos escrito en comparación con todo el boilerplate al que estás acostumbrado. La razón de esto es que todo lo que intentamos hacer de forma artificial en Redux es nativo en Elm, no requiere librerías externas ni voluntad para cumplir con todos las normas y convenciones que se deben seguir para hacer un Redux de forma correcta.

¿Qué más?

Si te has quedado con ganas de más ya puedes leer el siguiente artículo en el que evolucionaremos un poco este ejemplo para introducir algunos conceptos nuevos entre los que destacamos:

  • Mensajes parametrizados: para poder pasar datos a nuestras desde el DOM a nuestras funciones.
  • Conversiones: veremos como convertir strings a números de forma segura con Elm.
  • Tipos contenedor: Relacionado con las conversiones, introduciremos el tipo Result que forma parte de la librería standard de Elm y se usa extensivamente para encapsular resultado de operaciones que pueden fallar.