Características y ventajas del lenguaje Elm

Durante este último tiempo han estado saliendo muchas nuevas tecnologías alrededor del desarrollo web. Y es que ya sea a nivel de frameworks, librerías, arquitecturas y paradigmas, el desarrollo frontend no ha dejado de evolucionar en todas direcciones. Una de los motivos que ha marcado esta progresión han sido la optimización de los renderizados y el SEO, de tal forma que hoy contamos con librerías como React que no solo mejoran los tiempos de pintado en los navegadores si no que además nos proveen una forma de hidratar el HTML inicial generado por un servidor.

Pero hoy no hablaremos de React, sino de una tecnología que quizá no ha generado tanto impacto pero que ha servido de inspiración para crear frameworks tan populares como Redux. Hablamos de Elm lang, un lenguaje de programación construido sobre JavaScript y que pretende acercar las bondades de la funcional al desarrollo de aplicaciones de navegador.

Elm lang fue diseñado por Evan Czaplick como trabajo de final de tesis y no solo como un lenguaje puramente funcional si no con el reclamo de que no provocaba errores en tiempo de ejecución. Y es que al estar construido sobre un compilador robusto de tipos fuertes nos brinda la habilidad de poder capturar todos los potenciales errores en tiempo de compilación. En Elm lang, si un programa compila, funciona.

Hasta hace relativamente poco, si queríamos aprovechar gran parte de las ventajas de la programación funcional en el navegador, teníamos que recurrir a lenguajes como Purescript, pero en la actualidad es posible crear sitios web y aplicaciones de forma funcional gracias a Elm lang. Todo ello sin renunciar a la performance.

Instalación

Elm es casi más un entorno de trabajo que un simple lenguaje de programación como JavaScript. En él se integran muchas herramientas e incluso un gestor de paquetes que nos permite empezar a desarrollar muy rápido en cuanto lo instalamos.

Para instalar Elm, simplemente tenemos que ir a la página oficial y seguir las instrucciones según nuestro sistema operativo. Una vez que hemos instalado el entorno, ya estaremos listos para empezar a trabajar con Elm.

Todo el entorno de desarrollo se compone de los siguientes elementos:

  1. elm init: Responsable de crear proyectos. Nos agregará una carpeta src/ y un fichero elm.json (similar al package.json).
  2. elm make: Es el compilador que usaremos para compilar nuestros ficheros elm a código JavaScript listo para ser ejecutado en un navegador.
  3. elm install: Nos permitirá instalar paquetes usando el sistema de paquetes de Elm lang.
  4. elm reactor: Con el podremos ejecutar un servidor en el que veamos directamente el resultado de los cambios que hagamos en los ficheros elm, sin tener que hacer elm make cada vez.

Con estos cuatro componentes podremos realizar cualquier aplicación en Elm lang.

Empezar a usar Elm

La forma más sencilla de empezar a usar Elm y familiarizarse con la sintaxis es utilizar un quinto componente que viene cuando instalamos Elm y que no hemos mencionado arriba: elm repl. Y como su propio nombre indica, nos abre un intérprete de Elm.

La segunda forma, que es la que usaremos habitualmente cuando ya nos adaptemos a los básicos de Elm, es crear un proyecto y usar el reactor de Elm.

Para empezar, crear una nueva carpeta, entra en ella y ejecuta el siguiente comando en una terminal:

> elm init

Este programa creará el fichero elm.json e instalará los paquetes básicos para empezar a trabajar. Además, nos creará una carpeta src/ que es donde pondremos el código fuente.

Por tanto, podemos ahora crear un fichero Main.elm dentro de src/, con el siguiente contenido:

module Main exposing (..)
import Browser
import Html exposing (h1, text)

-- así ponemos un comentario

view _ =
    h1 [] [ text "Hello world" ]

main =
    Browser.sandbox { init = 0, view = view, update = (\_ _ -> 0) }

Si ahora ejecutamos en la terminal el comando:

> elm reactor

Nos ejecutará un servidor en el puerto 8000 (si no está ya ocupado). Si ahora vamos a http://localhost:8000/src/Main.elm deberíamos de ver un “Hello world” en el navegador.

Si ahora por ejemplo cambiamos el código anterior y metemos y cambiamos el valor de retorno de la función view a:

view _ =
    "Hello world"

Obtendremos un amigable error de compilación que nos dará pistas sobre como arreglarlo:

-- TYPE MISMATCH -- /src/Main.elm

The 1st argument to `sandbox` is not what I expect:

12|     Browser.sandbox { init = 0, view = view, update = (\_ _ -> 0) }
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This argument is a record of type:

    { init : number, update : msg -> number -> number, view : number -> String }

But `sandbox` needs the 1st argument to be:

    { init : number
    , update : msg -> number -> number
    , view : number -> Html.Html msg
    }

En este caso nos dice que la función sandbox espera que la función view tome un número y devuelva un Html.Html msg, pero nuestro view toma un número y devuelve un String, por lo que hay un Type mismatch.

Características del lenguaje Elm

Ahora que ya tenemos el entorno de desarrollo montado, vamos a pasar a comentar las características del lenguaje Elm y porque lo vuelven una alternativa muy interesante a otros lenguajes de programación web.

Como comentábamos antes, Elm es un lenguaje de programación funcional que nos ofrece:

  • Mensajes de error de compilación sencillos de entender.
  • No errores en tiempo de ejecución.
  • Fácil refactor gracias a su robusto sistema de tipos.
  • Interoperabilidad con JavaScript mediante puertos.

Y además es un lenguaje es fuertemente tipado y que viene acompañado con una arquitectura reactiva que renderiza directamente el modelo de datos en el navegador sin necesidad de tener que manipular el DOM.

Puramente funcional

Es cierto que cuando usamos lenguajes como JavaScript nos dicen que beben directamente del paradigma funcional, pero una cosa es poder aprovecharse superficialmente de diversas características de la programación funcional y otra muy distinta es tener un lenguaje de programación 100% construido para ser funcional.

Este es el caso de Elm, donde solo con funciones y composición de funciones podremos crear cualquier programa. Tanto es así, que casi todo en Elm es una función, desde los union types hasta los operadores para sumar (+) y restar (-).

Fuertemente tipado

Elm implementa un sistema de tipado estático que podría hacernos ver a las funciones y variables como si se tratasen de piezas de un puzle que solo pueden encajar unas con otras cuando los tipos coincidan. Por ejemplo, observemos la siguiente función:

count : List a -> Int
count list =
    List.length list

La función ‘count’ espera una lista de tipos ‘a’ (cualquier tipo) y devuelve un número entero. Lo que hemos definido encima de la función es lo que se conoce como una Type annotation y sirve para definir los tipos de variables y funciones. No es obligatorio ponerlas ya que Elm lang puede inferir los tipos, pero es muy recomendable.

Aquí ‘a’ actúa como un genérico y nos permite crear una función que funciona sobre una lista que contenga cualquier tipo (List Int, List Float, etc).

De igual forma, podemos definir una variable de la siguiente forma:

pi : Float
pi = 3.141516

Si por ejemplo ahora intentásemos redefinir la variable pi a otra cosa, obtendríamos un error debido a que en Elm todo es inmutable.

Veamos ahora esta otra función:

updateName : { name : String, lastname : String } -> String -> { name : String, lastname : String }
updateName user name =
    { user | name = name }

Esta nueva función tiene tres argumentos, un Record (similar a los objetos de JavaScript pero solo pueden guardar información, no funciones) formado por un nombre y un apellido (ambos del tipo String). Seguidamente toma un String (que representará el nuevo nombre) y devuelve una estructura igual a la entrada.

Normalmente cuando tratamos con Records no definimos los tipos así, sino que nos creamos un alias sobre ese tipo con un nombre mucho más amigable:

type alias User =
    { name : String
    , lastname : String
    }

Y ahora si refactorizamos la función ‘updateName’, quedaría así:

updateName : User -> String -> User
updateName user name =
    { user | name = name }

Finalmente, en el cuerpo de la función al hacer la “actualización”, Elm generará una nuevo Record User donde el nombre tenga el nuevo valor.

Arquitectura de Elm

Si bien los propios entresijos del lenguaje son interesantes, lo cierto es que bebe de muchos lenguajes de estilo funcional ya existentes, como Haskell o Lisp. Sin embargo, la característica principal que podemos decir que define a Elm es su arquitectura ya que integra muy bien el estilo funcional (con todas sus ventajas) en un flujo que a más de uno le sonará de otras tecnologías actuales. Y es que Elm ha servido de inspiración para muchas de las librerías que usamos actualmente, como por ejemplo Redux.

La arquitectura de Elm define un patrón para construir programas interactivos. Es algo que forma parte del lenguaje a diferencia por ejemplo de librerías como React que necesitamos instalarlas y configurarlas aparte (normalmente incluso con transpilación en el caso de JSX).

Un programa de Elm se rige por el principio básico de producir HTML que será imprimido por pantalla y posteriormente el programa recibirá mensajes sobre lo que está pasando, por ejemplo, cuando alguien pulsa sobre un botón.

Esto también se conoce como programación reactiva y sería similar a como en la arquitectura Flux se producen Actions que cambiaran un estado, solo que aquí el mensaje puede ser cualquier cosa no tanto una acción ligada a un evento de usuario o a un evento asíncrono.

Los tres conceptos fundamentales que definen el núcleo de esta arquitectura son:

  • El modelo: El estado de la aplicación.
  • La función vista: Una función que transforma el modelo en HTML.
  • La función de actualización: Una función que genera un nuevo modelo basado en mensajes.

Definir estos elementos en Elm es relativamente sencillo y nos permite tener una aplicación funcional en pocos pasos. Por ejemplo, con este código podemos definir el típico Contador:

import Browser
import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)

main =
  Browser.sandbox { init = 0, update = update, view = view }

type Msg = Increment | Decrement

update msg model =
  case msg of
    Increment ->
      model + 1

    Decrement ->
      model - 1

view model =
  div []
    [ button [ onClick Decrement ] [ text "-" ]
    , div [] [ text (String.fromInt model) ]
    , button [ onClick Increment ] [ text "+" ]
    ]

Hemos definido los tres elementos básicos (modelo, vista y update) y los hemos ido componiendo usando la función Browser.sandbox que nos permite inicializar una aplicación básica con la arquitectura de Elm.

Es más, si os fijáis ni siquiera necesitamos escribir HTML, puesto que la vista se conforma de un conjunto de funciones llamadas en jerarquía. Se acabó lo de usar múltiples lenguajes para programar y maquetar una página.

Conceptos básicos de Elm

Fuera de la arquitectura de Elm que está pensada para crear aplicaciones web reactivas, hay una serie de conceptos alrededor de Elm lang que como en otros lenguajes funcionales lo vuelven muy poderoso a la hora de modelar soluciones.

Por ejemplo, en JavaScript podríamos representar las acciones de los usuarios como un String con el nombre de la acción o incluso como un objeto con un tipo y un payload (como se define en Redux). Sin embargo, en Elm existe un mecanismo por el cual definir nuevos tipos que después se integrarán perfectamente con el lenguaje.

Tipos unión

Para crear un nuevo tipo en Elm tenemos dos opciones, o creamos un alias sobre un tipo existente (como hicimos con el record User) o definimos un union type que “una” diferentes variaciones en un mismo tipo.

Por ejemplo, en el código que teníamos arriba del contador, tenemos dos acciones: Increment y Decrement. Si queremos modelar esas acciones como mensajes que nuestro programa puede enviarnos (a través por ejemplo de un evento de usuario) podemos definir el tipo Msg de la siguiente forma:

type Msg = Increment | Decrement

Ahora podemos usar Increment y Decrement (ambos del tipo Msg) como si fueran constructores del tipo Msg. Por ejemplo, definamos una función con la siguiente anotación:

value : Msg -> Int

value es una función que recibe un valor del tipo Msg y devuelve un número entero. Con esto, podríamos definir el cuerpo de la función tal que así:

value : Msg -> Int
value msg =
    case msg of
        Increment -> 1
        Decrement -> -1

Como el tipo Msgtiene dos variaciones potenciales, necesitamos un mecanismo para poder “abrir” el tipo Msg y mirar cada una de ellas para poder realizar nuestra lógica. Ese mecanismo es un case of.

Un case of es la sintaxis por la cual podemos mirar dentro del tipo y obtener cada variación. Aquí el compilador de Elm nos va a obligar a especificar casos para cada variación, en este caso Increment y Decrement, por lo que, si no controlamos algún caso, el programa no compilará. Esta es una de las bases por las cuales no tenemos errores en tiempo de ejecución, ya que al obligarnos a controlar cada caso nunca tendremos errores del tipo, esto es undefined como en JavaScript.

Ahora con esta función podríamos guardar en una variable cada valor, por ejemplo:

positiveOne : Int
positiveOne = value Increment

negativeOne : Int
negativeOne = value Decrement

Ambas del tipo Int, como value devuelve un Int, los tipos coinciden y todo funciona.

Además, las variaciones de un tipo unión puede guardar información. Imaginemos que queremos guardar el número por el cuál vamos a decrementar o incrementar un valor:

type Msg = Increment Int | Decrement Int

Como el número será un entero, le ponemos Int. Ahora modificamos brevemente nuestra función value para cubrir las nuevas especificaciones:

value : Msg -> Int
value msg =
    case msg of
        Increment n -> n
        Decrement n -> -n

Donde n será nuestro valor y en ambos casos queremos devolverlo.

Ahora si usamos esta nueva función en variables, tendriamos:

positiveThree : Int
positiveThree = value (Increment 3)

negativeTwo : Int
negativeTwo = value (Decrement 2)

En este caso hemos usado los paréntesis para diferenciar los argumentos de value de los argumentos de Increment y Decrement.

Composición

En programación funcional todo es composición de funciones y tipos, y en Elm no podría ser de otra manera. Por ejemplo, imaginemos que tenemos estas dos funciones:

inc : Int -> Int
inc n = n + 1

double : Int -> Int
double n = n * 2

La función inc toma un entero y devuelve el resultado de incrementarlo en 1. Así mismo la función double coge un entero y devuelve su dobe. Si queremos ahora definir una función que duplique un número y lo incremente en 1, podemos hacerlo componiendo esas dos funciones para crear una nueva:

doubleInc : Int -> Int
doubleInc = double >> inc

Con el operador >> hemos creado una composición entre double y inc, de tal forma que tenemos una función nueva que toma un número, lo duplica y después lo incrementa en 1:

> doubleInc 2
5 : number

Cuando componemos funciones es como si tuviéramos piezas de un puzle donde cada argumento actúa como el conector de una pieza. Para poder unirla a otra, los conectores tienen que coincidir. Esto es si tengo una función F que toma un tipo A y devuelve un tipo B y después tengo una función G que toma un tipo B y devuelve un tipo C, la composición de F ∘ G será una función que tome un tipo A y devuelva un tipo C.

Si tenemos una función que devuelve B y tenemos otra que recibe B, es como si tuviéramos dos piezas de puzle que encajasen perfectamente.

La clave está en que a diferencia de un puzle donde cada pieza solo puede encajar con otra pieza determinada, las funciones pueden encajar con muchas otras funciones diferentes siempre que los tipos de salida de una coincidan con los tipos de entrada de otras.

Ventajas y desventajas

Como todo, el usar Elm viene con unos pros y unos contras que son importante conocer si queremos no ya usar Elm en nuestros proyectos, sino al menos darle un tiento a un lenguaje que puede hacernos pensar de forma diferente.

Las ventajas más importantes ya las hemos ido comentando a lo largo del artículo, pero las resumimos a continuación:

  • Generar programas que no fallan.
  • Olvidarnos de gestionar null o undefiendya que en Elm no existen valores nulos.
  • Maximizamos la reutilización gracias a la composición.
  • Amplio número de librerías.
  • Performance. Incluso comparados con las librerías más populares como React (Elm 0.17 es más rápido que React 15.3 según estos tests).
  • Fácil refactor gracias a su robusto sistema de tipos.
  • Buena comunidad.

Por otro lado, la desventaja mayor de usar Elm es su curva de aprendizaje sobre todo si es el primer lenguaje puramente funcional que tocamos. Y es que el modelo mental necesario para entender muchos de sus conceptos (como la composición o la transformación de valores), difiere mucho cuando lo comparamos por ejemplo con el modelo de programación tradicional u orientada a objetos.

Sin embargo, si ya has tocado otros lenguajes funcionales, te encontraras creando aplicaciones interactivas en pocos minutos, pues esta curva de aprendizaje que comentábamos antes se aplana mucho cuando ya tenemos la mente estructurada para pensar en funcional. Además, como Elm se basa en otros lenguajes como Haskell, encontraremos muchas similitudes.

Otra gran desventaja radica en la forma que tiene de codificar JSON a diferencia de que lo fácil y rápido que es trabajar con notación JSON en JavaScript, en Elm tendremos que aprender acerca de codificadores y decodificadores para poder trabajar con ellos, haciendo el trabajo con esta notación un poco más tediosa. Aunque este proceso le otorga una fiabilidad que por contra es difícil obtener cuando trabajo con JSON en JavaScript.

Conclusión

Como habéis podido ver, Elm es un lenguaje muy interesante que pretende acercar las grandes ventajas de un lenguaje funcional a los desarrolladores web. Ya sea que quieras usar Elm de forma profesional y a modo de experimentos en tus proyectos personas, te aseguro que la fiabilidad y comodidad de generar código con él, te enamorará.

Si bien es cierto que la curva de aprendizaje puede ser un poco empinada al principio, el camino se hace mucho más llevadero al contar con un compilador muy amigable de utilizar y que da tips al programador sobre cómo puede hacer que su código funcione. Es muy dinámico aprender cuando alguien te dice en que fallas y como arreglarlo.

Si quieres olvidarte de errores en tiempo de ejecución de una vez por todas, dadle una oportunidad a Elm lang.

También te puede interesar...

Programador JavaScript Profesional

Programador JavaScript Profesional

35 horas y 51 minutos · Carrera

Domina JavaScript, el lenguaje más demandado en la actualidad.

Lenguajes de Programación

Qué es la programación funcional y sus características

09 Septiembre 2022 Pablo Fernández
Fundamentos de SEO

Curso de fundamentos de SEO

2 horas y 10 minutos · curso

  • Herramientas

Las cookies nos permiten ofrecer nuestros servicios. Al utilizar nuestros servicios, aceptas el uso que hacemos de las cookies. Más Información.