Primeros pasos con el lenguaje de programación Elm
Publicado: 2022-03-11Cuando el desarrollador principal de un proyecto muy interesante e innovador sugirió cambiar de AngularJS a Elm, mi primer pensamiento fue: ¿Por qué?
Ya teníamos una aplicación AngularJS bien escrita que estaba en un estado sólido, bien probada y comprobada en producción. Y Angular 4, al ser una actualización digna de AngularJS, podría haber sido una elección natural para la reescritura, al igual que React o Vue. Elm parecía un lenguaje extraño de dominio específico del que la gente apenas había oído hablar.
Bueno, eso fue antes de saber nada sobre Elm. Ahora, con algo de experiencia con él, especialmente después de la transición desde AngularJS, creo que puedo dar una respuesta a ese "por qué".
En este artículo, analizaremos los pros y los contras de Elm y cómo algunos de sus conceptos exóticos se adaptan perfectamente a las necesidades de un desarrollador web front-end. Para ver un artículo sobre el lenguaje Elm más parecido a un tutorial, puede echar un vistazo a esta publicación de blog.
Elm: un lenguaje de programación puramente funcional
Si está acostumbrado a programar en Java o JavaScript y siente que es la forma natural de escribir código, aprender Elm puede ser como caer por la madriguera del conejo.
Lo primero que notará es la extraña sintaxis: sin llaves, muchas flechas y triángulos.
Puede aprender a vivir sin llaves, pero ¿cómo define y anida bloques de código? O, ¿dónde está el bucle for
, o cualquier otro bucle, para el caso? Si bien el alcance explícito se puede definir con un bloque let
, no hay bloques en el sentido clásico ni bucles en absoluto.
Elm es un lenguaje de cliente web puramente funcional, fuertemente tipado, reactivo y basado en eventos.
Puede comenzar a preguntarse si la programación es posible de esta manera.
En realidad, estas cualidades se suman para brindarle un asombroso paradigma de programación y desarrollo de buen software.
puramente funcional
Puede pensar que al usar la versión más nueva de Java o ECMAScript 6, puede hacer programación funcional. Pero, eso es solo la superficie.
En esos lenguajes de programación, aún tiene acceso a un arsenal de construcciones de lenguaje y la tentación de recurrir a las partes no funcionales del mismo. Donde realmente notas la diferencia es cuando no puedes hacer nada más que programación funcional. Es en ese punto donde eventualmente comienzas a sentir cuán natural puede ser la programación funcional.
En Elm, casi todo es una función. El nombre del registro es una función, el valor del tipo de unión es una función: cada función se compone de funciones parcialmente aplicadas a sus argumentos. Incluso los operadores como más (+) y menos (-) son funciones.
Para declarar un lenguaje de programación como puramente funcional, en lugar de la existencia de tales construcciones, la ausencia de todo lo demás es de suma importancia. Solo entonces puedes empezar a pensar de una manera puramente funcional.
Elm se basa en conceptos maduros de programación funcional y se parece a otros lenguajes funcionales como Haskell y OCaml.
fuertemente tipado
Si programa en Java o TypeScript, entonces sabe lo que esto significa. Cada variable debe tener exactamente un tipo.
Existen algunas diferencias, por supuesto. Al igual que con TypeScript, la declaración de tipo es opcional. Si no está presente, se inferirá. Pero no hay ningún tipo "cualquiera".
Java admite tipos genéricos, pero de una mejor manera. Los genéricos en Java se agregaron más tarde, por lo que los tipos no son genéricos a menos que se especifique lo contrario. Y, para usarlos, necesitamos la fea sintaxis <>
.
En Elm, los tipos son genéricos a menos que se especifique lo contrario. Veamos un ejemplo. Supongamos que necesitamos un método que tome una lista de un tipo particular y devuelva un número. En Java sería:
public static <T> int numFromList(List<T> list){ return list.size(); }
Y, en el idioma Elm:
numFromList list = List.length list
Aunque es opcional, le sugiero enfáticamente que siempre agregue declaraciones de tipo. El compilador de Elm nunca permitiría operaciones en tipos incorrectos. Para un ser humano, es mucho más fácil cometer ese error, especialmente mientras aprende el idioma. Entonces, el programa anterior con anotaciones de tipo sería:
numFromList: List a -> Int numFromList list = List.length list
Puede parecer inusual al principio declarar los tipos en una línea separada, pero después de un tiempo comienza a verse natural.
Idioma del cliente web
Lo que esto significa simplemente es que Elm compila en JavaScript, por lo que los navegadores pueden ejecutarlo en una página web.
Dado eso, no es un lenguaje de propósito general como Java o JavaScript con Node.js, sino un lenguaje específico de dominio para escribir la parte del cliente de las aplicaciones web. Aún más que eso, Elm incluye escribir la lógica empresarial (lo que hace JavaScript) y la parte de presentación (lo que hace HTML), todo en un lenguaje funcional.
Todo esto se hace de una manera similar a un marco muy específico, que se llama The Elm Architecture.
Reactivo
Elm Architecture es un marco web reactivo. Cualquier cambio en los modelos se representa inmediatamente en la página, sin manipulación DOM explícita.
De esa manera, es similar a Angular o React. Pero Elm también lo hace a su manera. La clave para comprender sus conceptos básicos está en la firma de las funciones de view
y update
:
view : Model -> Html Msg update : Msg -> Model -> Model
Una vista de Elm no es solo la vista HTML del modelo. Es HTML que puede producir mensajes del tipo Msg
, donde Msg
es un tipo de unión exacto que defina.
Cualquier evento de página estándar puede producir un mensaje. Y, cuando se produce un mensaje, Elm llama internamente a la función de actualización con ese mensaje, que luego actualiza el modelo en función del mensaje y el modelo actual, y el modelo actualizado se vuelve a representar internamente en la vista.
Evento conducido
Al igual que JavaScript, Elm se basa en eventos. Pero a diferencia, por ejemplo, de Node.js, donde se proporcionan devoluciones de llamada individuales para las acciones asincrónicas, los eventos de Elm se agrupan en conjuntos discretos de mensajes, definidos en un tipo de mensaje. Y, como con cualquier tipo de unión, la información que llevan los valores de tipos separados podría ser cualquier cosa.
Hay tres fuentes de eventos que pueden producir mensajes: acciones del usuario en la vista Html
, ejecución de comandos y eventos externos a los que nos suscribimos. Es por eso que los tres tipos, Html
, Cmd
y Sub
contienen msg
como argumento. Y el tipo de Msg
msg
con M mayúscula), donde se centraliza todo el procesamiento de mensajes.
Código fuente de un ejemplo realista
Puede encontrar un ejemplo completo de aplicación web de Elm en este repositorio de GitHub. Aunque simple, muestra la mayor parte de la funcionalidad que se usa en la programación diaria del cliente: recuperación de datos de un extremo REST, decodificación y codificación de datos JSON, uso de vistas, mensajes y otras estructuras, comunicación con JavaScript y todo lo que se necesita para compilar y empaquetar. Código Elm con Webpack.
La aplicación muestra una lista de usuarios recuperada de un servidor.
Para un proceso de configuración/demostración más sencillo, el servidor de desarrollo de Webpack se utiliza tanto para empaquetar todo, incluido Elm, como para servir la lista de usuarios.
Algunas funciones están en Elm y otras en JavaScript. Esto se hace intencionalmente por una razón importante: mostrar la interoperabilidad. Probablemente desee probar Elm para comenzar, o cambiar gradualmente el código JavaScript existente, o agregar una nueva funcionalidad en el lenguaje Elm. Con la interoperabilidad, su aplicación continúa funcionando con código Elm y JavaScript. Este es probablemente un mejor enfoque que iniciar toda la aplicación desde cero en Elm.
La parte de Elm en el código de ejemplo se inicializa primero con datos de configuración de JavaScript, luego se recupera la lista de usuarios y se muestra en el lenguaje de Elm. Digamos que tenemos algunas acciones de usuario ya implementadas en JavaScript, por lo que invocar una acción de usuario en Elm solo despacha la llamada.
El código también usa algunos de los conceptos y técnicas que se explican en la siguiente sección.
Aplicación de los conceptos de Elm
Repasemos algunos de los conceptos exóticos del lenguaje de programación Elm en escenarios del mundo real.
Tipo de unión
Este es el oro puro del idioma Elm. ¿Recuerda todas esas situaciones en las que era necesario utilizar datos estructuralmente diferentes con el mismo algoritmo? Siempre es difícil modelar esas situaciones.
Aquí hay un ejemplo: imagina que estás creando una paginación para tu lista. Al final de cada página, debe haber enlaces a las páginas anterior, siguiente y todas por sus números. ¿Cómo lo estructura para contener la información de qué enlace hizo clic el usuario?
Podemos usar varias devoluciones de llamada para clics anteriores, siguientes y de número de página, o podemos usar uno o dos campos booleanos para indicar en qué se hizo clic, o dar un significado especial a valores enteros particulares, como números negativos, cero, etc. Pero ninguno de estas soluciones pueden modelar exactamente este tipo de evento de usuario.
En Elm, es muy simple. Definiríamos un tipo de unión:
type NextPage = Prev | Next | ExactPage Int
Y lo usamos como parámetro para uno de los mensajes:
type Msg = ... | ChangePage NextPage
Finalmente, actualizamos la función para tener un case
para verificar el tipo de nextPage
:
update msg model = case msg of ChangePage nextPage -> case nextPage of Prev -> ... Next -> ... ExactPage newPage -> ...
Hace las cosas muy elegantes.
Creación de múltiples funciones de mapa con <|
Muchos módulos incluyen una función de map
, con varias variantes para aplicar a un número diferente de argumentos. Por ejemplo, List
tiene map
, map2
, … , hasta map5
. Pero, ¿y si tenemos una función que toma seis argumentos? No hay map6
. Pero, hay una técnica para superar eso. Utiliza el <|
función como parámetro, y funciones parciales, con algunos de los argumentos aplicados como resultados intermedios.
Para simplificar, supongamos que List
solo tiene map
y map2
, y queremos aplicar una función que toma tres argumentos en tres listas.
Así es como se ve la implementación:
map3 foo list1 list2 list3 = let partialResult = List.map2 foo list1 list2 in List.map2 (<|) partialResult list3
Supongamos que queremos usar foo
, que simplemente multiplica sus argumentos numéricos, definidos como:
foo abc = a * b * c
Así que el resultado de map3 foo [1,2,3,4,5] [1,2,3,4,5] [1,2,3,4,5]
es [1,8,27,64,125] : List number
.
Vamos a deconstruir lo que está pasando aquí.
Primero, en partialResult = List.map2 foo list1 list2
, foo
se aplica parcialmente a cada par en list1
y list2
. El resultado es [foo 1 1, foo 2 2, foo 3 3, foo 4 4, foo 5 5]
, una lista de funciones que toma un parámetro (ya que los dos primeros ya están aplicados) y devuelve un número.
A continuación, en List.map2 (<|) partialResult list3
, en realidad es List.map2 (<|) [foo 1 1, foo 2 2, foo 3 3, foo 4 4, foo 5 5] list3
. Para cada par de estas dos listas, llamamos a la función (<|)
. Por ejemplo, para el primer par, es (<|) (foo 1 1) 1
, que es lo mismo que foo 1 1 <| 1
foo 1 1 <| 1
, que es lo mismo que foo 1 1 1
, que produce 1
. Para el segundo, será (<|) (foo 2 2) 2
, que es foo 2 2 2
, que se evalúa como 8
, y así sucesivamente.
Este método puede ser particularmente útil con funciones mapN
para decodificar objetos JSON con muchos campos, ya que Json.Decode
los proporciona hasta map8
.
Eliminar todos los valores Nothing de una lista de Maybes
Digamos que tenemos una lista de valores de Maybe
y queremos extraer solo los valores de los elementos que tienen uno. Por ejemplo, la lista es:
list : List (Maybe Int) list = [ Just 1, Nothing, Just 3, Nothing, Nothing, Just 6, Just 7 ]
Y queremos obtener [1,3,6,7] : List Int
. La solución es esta expresión de una línea:
List.filterMap identity list
Veamos por qué esto funciona.

List.filterMap
espera que el primer argumento sea una función (a -> Maybe b)
, que se aplica a los elementos de una lista proporcionada (el segundo argumento), y la lista resultante se filtra para omitir todos los valores de Nothing
, y luego el real los valores se extraen de Maybe
s.
En nuestro caso, proporcionamos la identity
, por lo que la lista resultante es nuevamente [ Just 1, Nothing, Just 3, Nothing, Nothing, Just 6, Just 7 ]
. Después de filtrarlo, obtenemos [ Just 1, Just 3, Just 6, Just 7 ]
, y después de la extracción de valores, es [1,3,6,7]
, como queríamos.
Decodificación JSON personalizada
A medida que nuestras necesidades en la decodificación (o deserialización) de JSON comiencen a exceder lo expuesto en el módulo Json.Decode
, es posible que tengamos problemas para crear nuevos decodificadores exóticos. Esto se debe a que estos decodificadores se invocan desde la mitad del proceso de decodificación, por ejemplo, dentro de los métodos Http
, y no siempre está claro cuáles son sus entradas y salidas, especialmente si hay muchos campos en el JSON proporcionado.
Aquí hay dos ejemplos para mostrar cómo manejar tales casos.
En el primero, tenemos dos campos en el JSON entrante, a
y b
, que indican los lados de un área rectangular. Pero, en un objeto Elm, solo queremos almacenar su área.
import Json.Decode exposing (..) areaDecoder = map2 (*) (field "a" int) (field "b" int) result = decodeString areaDecoder """{ "a":7,"b":4 }""" -- Ok 28 : Result.Result String Int
Los campos se decodifican individualmente con el decodificador field int
y luego ambos valores se proporcionan a la función proporcionada en map2
. Como la multiplicación ( *
) también es una función, y toma dos parámetros, podemos usarla así. El areaDecoder
resultante es un decodificador que devuelve el resultado de la función cuando se aplica, en este caso, a*b
.
En el segundo ejemplo, obtenemos un campo de estado desordenado, que puede ser nulo, o cualquier cadena, incluido el vacío, pero sabemos que la operación tuvo éxito solo si está "bien". En ese caso, queremos almacenarlo como True
y para todos los demás casos como False
. Nuestro decodificador se ve así:
okDecoder = nullable string |> andThen (\ms -> case ms of Nothing -> succeed False Just s -> if s == "OK" then succeed True else succeed False )
Apliquémoslo a algunos JSON:
decodeString (field "status" okDecoder) """{ "a":7, "status":"OK" }""" -- Ok True decodeString (field "status" okDecoder) """{ "a":7, "status":"NOK" }""" -- Ok False decodeString (field "status" okDecoder) """{ "a":7, "status":null }""" -- Ok False
La clave aquí está en la función suministrada a andThen
, que toma el resultado de un decodificador de cadena anulable anterior (que es un Maybe String
), lo transforma en lo que necesitemos y devuelve el resultado como un decodificador con la ayuda de succeed
.
Punto clave
Como se puede ver en estos ejemplos, la programación de forma funcional puede no ser muy intuitiva para los desarrolladores de Java y JavaScript. Se necesita algo de tiempo para acostumbrarse, con mucho ensayo y error. Para ayudar a comprenderlo, puede usar elm-repl
para ejercitar y verificar los tipos de devolución de sus expresiones.
El proyecto de ejemplo vinculado anteriormente en este artículo contiene varios ejemplos más de decodificadores y codificadores personalizados que también pueden ayudarlo a comprenderlos.
Pero aún así, ¿por qué elegir Elm?
Al ser tan diferente de otros marcos de clientes, el lenguaje Elm ciertamente no es "otra biblioteca de JavaScript". Como tal, tiene muchos rasgos que pueden considerarse positivos o negativos en comparación con ellos.
Comencemos con el lado positivo primero.
Programación de clientes sin HTML y JavaScript
Finalmente, tienes un lenguaje donde puedes hacerlo todo. No más separación y combinaciones incómodas de su mezcla. Sin generar HTML en JavaScript y sin lenguajes de plantillas personalizados con algunas reglas lógicas simplificadas.
Con Elm, solo tiene una sintaxis y un idioma, en todo su esplendor.
Uniformidad
Como casi todos los conceptos se basan en funciones y algunas estructuras, la sintaxis es muy concisa. No tiene que preocuparse si algún método está definido en el nivel de instancia o clase, o si es solo una función. Todas son funciones definidas a nivel de módulo. Y no hay cien formas diferentes de iterar listas.
En la mayoría de los idiomas, siempre existe este argumento sobre si el código está escrito en la forma del idioma. Hay que dominar muchos modismos.
En Elm, si compila, probablemente sea de la manera "Elm". Si no, bueno, ciertamente no lo es.
Expresividad
Aunque concisa, la sintaxis de Elm es muy expresiva.
Esto se logra principalmente mediante el uso de tipos de unión, declaraciones de tipos formales y el estilo funcional. Todos estos inspiran el uso de funciones más pequeñas. Al final, obtienes un código que es prácticamente autodocumentado.
sin nulo
Cuando usa Java o JavaScript durante mucho tiempo, null
se convierte en algo completamente natural para usted, una parte inevitable de la programación. Y, aunque constantemente vemos NullPointerException
y varios TypeError
, todavía no creemos que el verdadero problema sea la existencia de null
. Es tan natural .
Se vuelve claro rápidamente después de un tiempo con Elm. No tener null
no solo nos evita ver errores de referencia nula en tiempo de ejecución una y otra vez, sino que también nos ayuda a escribir un mejor código al definir y manejar claramente todas las situaciones en las que es posible que no tengamos el valor real, lo que también reduce la deuda técnica al no posponer el null
. manipulación hasta que algo se rompa.
La confianza de que funcionará
La creación de programas JavaScript sintácticamente correctos se puede hacer muy rápidamente. Pero, ¿realmente funcionará? Bueno, veamos después de recargar la página y probarla a fondo.
Con Elm, es todo lo contrario. Con la verificación de tipos estáticos y las verificaciones null
forzadas, se necesita algo de tiempo para compilar, especialmente cuando un principiante escribe un programa. Pero, una vez compilado, hay una buena posibilidad de que funcione correctamente.
Rápido
Este puede ser un factor importante a la hora de elegir un marco de cliente. La capacidad de respuesta de una aplicación web extensa suele ser crucial para la experiencia del usuario y, por lo tanto, para el éxito de todo el producto. Y, como muestran las pruebas, Elm es muy rápido.
Ventajas de Elm frente a los marcos tradicionales
La mayoría de los marcos web tradicionales ofrecen herramientas poderosas para la creación de aplicaciones web. Pero ese poder tiene un precio: arquitectura demasiado complicada con muchos conceptos y reglas diferentes sobre cómo y cuándo usarlos. Se necesita mucho tiempo para dominarlo todo. Hay controladores, componentes y directivas. Luego, están las fases de compilación y configuración, y la fase de ejecución. Y luego, están los servicios, las fábricas y todo el lenguaje de plantilla personalizado que se usa en las directivas proporcionadas: todas esas situaciones en las que necesitamos llamar a $scope.$apply()
directamente para que la página se actualice, y más.
La compilación de Elm a JavaScript ciertamente también es muy complicada, pero el desarrollador está protegido de tener que conocer todos los detalles. Solo escribe algo de Elm y deja que el compilador haga su trabajo.
Y, ¿por qué no elegir el olmo?
Ya basta de alabar a Elm. Ahora, veamos algo de su lado no tan bueno.
Documentación
Este es realmente un problema importante. El lenguaje Elm carece de un manual detallado.
Los tutoriales oficiales simplemente hojean el idioma y dejan muchas preguntas sin respuesta.
La referencia oficial de la API es aún peor. Muchas funciones carecen de cualquier tipo de explicación o ejemplos. Y luego están aquellos con la oración: “Si esto es confuso, siga el tutorial de arquitectura de Elm. ¡Realmente ayuda!” No es la mejor línea que desea ver en los documentos oficiales de la API.
Con suerte, esto cambiará pronto.
No creo que Elm se pueda adoptar ampliamente con una documentación tan pobre, especialmente con personas que vienen de Java o JavaScript, donde tales conceptos y funciones no son nada intuitivos. Para comprenderlos, se necesita una documentación mucho mejor con muchos ejemplos.
Formato y espacios en blanco
Deshacerse de llaves o paréntesis y usar espacios en blanco para la sangría podría verse bien. Por ejemplo, el código de Python se ve muy bien. Pero para los creadores de elm-format
, no fue suficiente.
Con todos los espacios de doble línea y las expresiones y asignaciones divididas en varias líneas, el código de Elm se ve más vertical que horizontal. Lo que sería una sola línea en el viejo C puede extenderse fácilmente a más de una pantalla en el lenguaje Elm.
Esto puede sonar bien si le pagan por líneas de código escritas. Pero, si desea alinear algo con una expresión que comenzó 150 líneas antes, buena suerte para encontrar la sangría correcta.
Manejo de registros
Es difícil trabajar con ellos. La sintaxis para modificar el campo de un registro es fea. No existe una manera fácil de modificar campos anidados o de hacer referencia arbitrariamente a campos por nombre. Y si está utilizando las funciones de acceso de forma genérica, hay muchos problemas para escribir correctamente.
En JavaScript, un registro u objeto es la estructura central que se puede construir, acceder y modificar de muchas maneras. Incluso JSON es solo una versión serializada de un registro. Los desarrolladores están acostumbrados a trabajar con registros en la programación web, por lo que las dificultades en su manejo en Elm pueden ser notables si se utilizan como estructura de datos principal.
Más mecanografía
Elm requiere que se escriba más código que JavaScript.
No hay una conversión de tipo implícita para las operaciones de cadenas y números, por lo que se necesitan muchas conversiones int-float y especialmente llamadas toString
, que luego requieren paréntesis o símbolos de aplicación de funciones para que coincidan con la cantidad correcta de argumentos. Además, la función Html.text
requiere una cadena como argumento. Se necesitan muchas expresiones de mayúsculas y minúsculas, para todos esos Maybe
s, Results
, tipos, etc.
La razón principal de esto es el estricto sistema de tipos, y ese puede ser un precio justo a pagar.
Decodificadores y codificadores JSON
Un área donde realmente se destaca más la escritura es el manejo de JSON. Lo que es simplemente una llamada JSON.parse()
en JavaScript puede abarcar cientos de líneas en el lenguaje Elm.
Por supuesto, se necesita algún tipo de mapeo entre las estructuras JSON y Elm. Pero la necesidad de escribir decodificadores y codificadores para la misma pieza de JSON es un problema grave. Si sus API REST transfieren objetos con cientos de campos, será mucho trabajo.
Envolver
Hemos visto sobre Elm, es hora de responder a las conocidas preguntas, probablemente tan antiguas como los propios lenguajes de programación: ¿Es mejor que la competencia? ¿Deberíamos usarlo en nuestro proyecto?
La respuesta a la primera pregunta puede ser subjetiva, ya que no toda herramienta es un martillo y no todo es un clavo. Elm puede brillar y ser una mejor opción sobre otros marcos de clientes web en muchos casos, mientras que en otros se queda corto. Pero ofrece un valor realmente único que puede hacer que el desarrollo web front-end sea mucho más seguro y fácil que las alternativas.
Para la segunda pregunta, para evitar responder con el también secular “depende”, la respuesta simple es: Sí. Incluso con todas las desventajas mencionadas, solo la confianza que Elm le brinda acerca de que su programa es correcto es razón suficiente para usarlo.
Codificar en Elm también es divertido. Es una perspectiva totalmente nueva para cualquiera que esté acostumbrado a los paradigmas de programación web "convencionales".
En el uso real, no tiene que cambiar toda su aplicación a Elm inmediatamente o comenzar una nueva completamente en ella. Puede aprovechar su interoperabilidad con JavaScript para probarlo, comenzando con una parte de la interfaz o alguna funcionalidad escrita en el lenguaje Elm. Descubrirá rápidamente si se adapta a sus necesidades y luego ampliará su uso o lo dejará. Y quién sabe, quizás también te enamores del mundo de la programación web funcional.