Elm - functional language

Introducción

El lenguaje ELM es un lenguaje cliente de web puramente funcional, fuertemente tipado, reactivo y basado en eventos ¿pero que queremos decir con esto?

  • Lenguaje puramente funcional: En ELM practicamente todo es una función, los nombres se tratan como funciones, el valor de union de tipos es una funcion, y cada función esta compuesta por funciones parciales aplicadas a sus argumentos, hasta los operadores (+) o (-) son tratados como funciones.
  • Fuertemente tipado: En ELM los tipos son genericos a menos que se espicifique lo contrario, aun así el lenguaje ELM no permite operaciones en los tipos incorrectos, es por eso que se recomienda siempre especificar los tipos.
  • Lenguaje cliente de web: ELM compila a javascript, es por eso que los buscadores pueden ejecutar este lenguaje en paginas web, de hecho su mayor uso se encuentra en esa area.
  • Reactivo: En ELM cualquier cambio ejecutado en los modelos es renderizado automaticamente en la vista, sin una manipulación explicita del DOM.
  • Basado en eventos: La base de ELM son los eventos, cada evento en ELM se agrupan en conjuntos discretos de mensajes, definidos en un tipo de mensaje. Existen tres fuentes de eventos que pueden producir mensajes: Las acciones de los usuarios en la vista HTML, La ejecución de comandos, Eventos a los cuales se subscriben.

Arquitectura ELM

La arquitectura ELM es un patrón para la creación de programas interactivos. El patrón básico de la arquitectura se ve de la siguiente manera:

El programa elm produce HTML para renderizar en pantalla, y luego el computador enviá un mensaje acerca de lo que esta sucediendo, es decir se crea una via de comunicación bidireccional que garantiza reactividad en cada programa creado por ELM. De esta forma la arquitectura básica de un programa ELM se divide en tres partes:

  • Modelo: Representa el estado del programa.
  • Vista: La forma de transformar el estado en HTML.
  • Actualización: una forma de actualizar el estado basado en el mensaje.
El siguiente ejemplo muestra como funciona la arquitectura:
module Main exposing (...) import Browser import Html exposing (Html, button, div, text) import Html.Events exposing (onClick) -- MAIN main = Browser.sandbox(init = init, update = update, view = view) type alias Model = Int init : Model init = 0 -- UPDATE type = Msg = Increment | Decrement update : Msg -> Model -> Model update msg model = case msg of Increment -> model + 1 Decrement -> model + 1 -- VIEW view : Model -> Html Msg view model = div [] [ button [ onClick Decrement ] [ text "-" ] , div [] [ text (String.fromInt model) ] , button [ onClick Increment ] [ test "+" ] ]
En el anterior código tenemos un contador en la parte del modelo, inicializado en 0, el cual se actualiza por medio de mensajes incrementando o decrementando, y por ultimo tenemos la renderización de este contador, junto a dos botones para modificar el valor. Para mas ejemplos como el anterior, siga al enlace Ejemplos ELM

Conceptos Exóticos

Tipos de uniones

Este concepto es la piedra preciosa detrás de ELM. Veamos este concepto con un ejemplo:

Imagine que usted esta creando una paginación para una lista. Al final de la pagina debe existir botones para devolverse, ir hacia adelante, e ir a cualquier pagina por su número, pero el problema es como mantener la información de cual link fue oprimido de una forma elegante. Podríamos utilizar múltiples callbacks para cada caso, o complicarnos con valores booleanos que indiquen en que tipo de acción nos encontramos, pero en ELM las cosas se hacen de una forma mas elegante. En ELM definiremos el siguiente tipo de union

type NextPage = Prev | Next | ExactPage Int

y lo utilizamos como un parámetro en el envió de mensaje de la siguiente manera:

type Msg = ... | ChangePage NextPage

Finalmente en la función de actualización discriminamos cada caso de la siguiente forma:

update msg model = case msg of ChangePage nextPage -> case nextPage of Prev -> ... Next -> ... ExactPage newPage -> ...

Creando múltiples mapeos de funciones con <|

Veamos este concepto por medio de un ejemplo: suponga que una lista tiene map, map2,...,map5. Pero ¿que sucede si tenemos una función que toma seis argumentos? No existe ningún map6. Pero, existe una técnica para solucionar esto. Usar la función <| como parámetro, y funciones parciales, con algunos de los argumentos aplicados como resultados parciales.

Por simplicidad, suponga que la lista solo tiene map y map2, y queremos aplicar una función que toma tres argumentos.

map3 foo list1 list2 list3 = let partialResult = List.map2 foo list1 list2 in List.map2 (<|) partialResult list3

Suponga que queremos usar foo, que solo multiplica los argumentos numéricos, definido como:

foo a b c = a * b * c

Así, 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].

De esta forma reconstruyamos lo que esta pasando en la parte anterior. Primero, en partialResult = List.map2 foo list1 list2, foo se aplica parcialmente a toda pareja 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 y retorna un numero, el cual es después llamado en List.map2 (<|) partialResult list3.

CÓDIGO ELM

Operadores, transformaciones y comparadores

Al igual que otros lenguajes, Elm soporta una gran variedad de operaciones entre valores, transformaciones de tipo y comparadores de expresiones. La siguiente tabla muestra algunos de los más representativos.

El núcleo del lenguaje

Es importante conocer ciertas estructuras que son útiles al momento de realizar código en Elm:

Values:

Pueden ser entendidos como literales, por ejemplo números enteros o cadenas definidas en el código.

Funciones:

Son las estructuras más importantes en el lenguaje, su estructura puede parecer extraña inicialmente debido a que difiere de otros lenguajes, sin embargo el concepto es exactamente el mismo. Declaración de la función “greet” que recibe el argumento “name”

> green name = | "Hello " ++ name ++ "!"

Declaración de la función “sum” que recibe dos argumentos “a” y “b”

> sum a b = | a + b

Condicionales:

La estructura de un condicional en Elm es muy similar a otros lenguajes como python. En el siguiente ejemplo se construye la función “greet” que recibe el parámetro “name”, y en el cuerpo de la función se hace un condicional para que varíe el output en función del valor de “name”.

> greet name = if name == "Felipe" then "Hola profesor!" else if name == "Laura" then "Hola monitora!" else "Hola " ++ name ++ "!"

Listas:

La declaración de las listas en Elm es equivalente a la declaración de una lista en lenguajes como python o javascript, con la diferencia de que en Elm todos los elementos de la lista deben ser del mismo tipo.

> list = ["A", "B", "C"] ["A", "B", "C"] > list = [1, 2, 3] [1, 2, 3]

Tuplas:

Las tuplas se utilizan principalmente cuando se requiere retornar varios valores en una misma función, tal vez de distinto tipo. Únicamente pueden tener dos o tres valores.

> ( True, "Lenguajes" ) ([1, 2, 3], "Lenguajes" > ( 1, False, "A" ) (1, False, "A")

Records:

El concepto de un registro o record en Elm es muy similar al de un objeto en Javascript o un diccionario en Python. Se refiere a un elemento que posee varios atributos con sus respectivos valores.

> chloe = | { edad = 1, | especie = "gato", | color = "gris" }

La forma de acceder a sus atributos es muy común en otros lenguajes

> chloe.edad 1 > chloe.especie "gato" > .color chloe "gris"

Cuando se quiere reescribir uno de sus atributos, en Elm se crea una copia completa del objeto variando únicamente los campos que le especificamos. Esto con el objetivo de que no se afecten las salidas de otras funciones que dependan del record inicial. En la siguiente línea de código se modifica la edad del record chloe para que sea 2 en lugar de 1.

> { chloe | edad = 2 }

Tipos:

Uno de los puntos más fuertes de Elm es que tenemos control total sobre los tipos de dato que hay en las funciones, tanto como en su entrada como en su salida. Sin embargo, a veces puede ser más útil definir nuestros propios tipos aparte de los nativos, para esto Elm nos da ciertas herramientas:

  • Type alias: Es muy útil para definir tipos que serán usados en funciones como parámetro o como salida. Es un concepto similar a la creación del modelo de un objeto, por lo que se debe especificar los tipos de los atributos que posea
    > type alias Gato = | { edad = Int, | nombre = String }
  • Custom types: Esta herramienta nos permite crear tipos personalizados de forma sencilla, estos pueden ser usados incluso como tipos de un Type alias.
    > type Raza = Criollo | Persa

Case y wildcard:

Imaginemos que tenemos un type alias “Gato” que tiene como atributos “raza” de tipo “Raza” (definido en el último fragmento de código) y de un nombre tipo cadena.

> type alias Gato = | { raza = Raza, | nombre = String }

La siguiente función identifica si un gato es de Raza Criollo o de Raza Persa

> esCriollo gato = | case gato of | Criollo _ -> "Es criollo!" | Persa _ -> "Es persa!" |

La estructura case sirve para tomar decisiones en base a la estructura de los datos de “gato”, sin embargo también sirve para tomar decisiones en base a valores, como lo sería una estructura switch-case existente en otros lenguajes. El carácter “_” sirve como comodín, indica que lo que sea que vaya en esa posición realmente no va a ser significativo en el flujo de la función, a esto se le conoce como wildcard.

Maybe

Un tipo Maybe sirve para indicar que un valor puede que no sea conocido.

> type Maybe a = | Just a | | Nothing

Por ejemplo, continuando con el ejemplo del type alias “Gato”, la siguiente definición significa que tal vez no se conozca la edad del gato, es importante haber definido ya el tipo Maybe.

> type alias Gato = | { nombre: String, | edad: Maybe Int }

Y ya instanciando dos elementos Gato, el primero con edad desconocida y el segundo con edad conocida

> tail = { nombre = "Tail", edad = Nothing }
> chloe = { nombre = "Chloe", edad = Just 1 }

Este concepto es bastante útil al definir atributos opcionales en records o cuando se requiere que una función no procese ciertos valores (por ejemplo, cuando se intenta hacer un parseo de String a Int pero el valor de la cadena no tiene representación entera)

Pipelines:

Tenemos dos funciones

> sum1 : Int -> Int | sum1 numero = | < function > > mult2 : Int -> Int | mult2 numero = | numero * 2 | < function >

Implementamos una función que las use para transformar un número entero

> transformar : Int -> Int | transformar numero = | mult2 (sum1 numero) |

Elm además nos provee de una herramienta llamada Pipelines para que los múltiples llamados a funciones sean más legibles para el programador, así entonces el último bloque de código quedaría de la siguiente forma

> transformar : Int -> Int | transformar numero = | numero |> sum1 |> mult2 |

PROGRAMACIÓN FUNCIONAL

¿Y los ciclos?

Elm no provee una implementación de ciclos de forma nativa. Si se requiere usar un ciclo en Elm, entonces se le pide al programador que haga uso de herramientas como la recursión. Así, entonces un ciclo construido de la siguiente forma en Javascript

var saludo = "" for (var i = 5; i> 0; i--){ saludo = saludo + ("hola:"+i+" "); }

En Elm podría ser implementado de la siguiente forma

> saludar: Int -> String | saludar num = | case num of | 1 -> "hola:1 " | _ -> "hola:" ++ String.fromInt num ++ " " ++ saludar (num-1)

El primer bloque almacena en la variable “saludo” el valor

“hola:5 hola:4 hola:3 hola:2 hola:1”

Este valor también es el resultante de llamar a la función “saludar” del segundo bloque utilizando el parámetro 5.

Funciones de primera clase

Elm tiene soporte para funciones de primera clase, esto significa que las funciones pueden ser asignadas a variables o ser usadas como atributos o valores de retorno en otras funciones. En el siguiente ejemplo se asigna a una variable una función

> sum a b = a + b < function > : number -> number -> number > miFunc = sum < function > : number -> number -> number

Funciones de orden superior

Son funciones que pueden tomar otras funciones como parámetros o retornar otras funciones. Esto es bastante evidente al saber que Elm realmente solo admite funciones que tienen un argumento, el lenguaje se encarga de hacer la conversión de una función que tiene varios parámetros a una lista de “curried functions”, donde cada una de las funciones retorna a su vez una función. Para ilustrar el concepto, veamos un ejemplo de implementación de una función que suma dos valores “a” y “b” en Elm y la respectiva conversión que se hace “por debajo” a “curried functions”. A continuación la implementación en Elm

> sum a b = a + b < function > : number -> number -> number > sum 5 2 7 : number > (sum 5) 2 7 : number > 2 |> (sum 5) 7 : number

La función “sum” es transformada de la siguiente forma. (Para hacer más entendible el código, se hizo la conversión equivalente en Javascript) La función sumar

function sumar (a, b) { return a + b; }

Es transformada en “curried functions”, una lista de funciones anidadas que a su vez retornan funciones. Cada una de estas funciones son funciones de orden superior.

function sumar (a) { return function (b) { return a + b; } }

Funciones lambda

Son cierto tipo especial de funciones que son usados en alguno de los siguientes escenarios:

  • Se quiere usar sólo una vez
  • La función es demasiado simple
  • Se requiere legibilidad (se definen sólamente cuando se necesitan)
  • Se necesita retornar una función desde una función

Por ejemplo, la siguiente función lambda es equivalente a una función que calcula el cuadrado de un número

> \n -> n*n < function >

Y la siguiente función retorna una función lambda

> suma a = | \b -> a + b | < function >

EJEMPLO FUNCIONAL

Dado un número natural n, par mayor que 2, calcular dos primos tales que su suma sea igual a n:
goldbach : Int -> Maybe (Int, Int) goldbach n = if isOdd n || n < 3 then Nothing else goldbach1 n <| primesInRange 2 n goldbach1 : Int -> List Int -> Maybe (Int, Int) goldbach1 n ps = case ps of [] -> Nothing p1 :: xs -> let ps1 = dropWhile (\y -> p1 + y < n) ps in case List.head ps1 of Nothing -> goldbach1 n xs Just p2 -> if p2 + p1 == n then Just (p1, p2) else goldbach1 n xs primesInRange : Int -> Int -> List Int primesInRange low high = if high < low || high < 2 then [] else dropWhile (\x -> x < low) <| primes high -- find all primes up to n primes : Int -> List Int primes n = if n < 2 then [] else eratos [2..n] [] -- sieve of Eratosthenes -- remove all the the non-primes from a list eratos : List Int -> List Int -> List Int eratos candidates primes = case candidates of [] -> List.reverse primes x::xs -> let cs = List.filter (\y -> (y % x) /= 0) xs in eratos cs (x :: primes) dropWhile : (a -> Bool) -> List a -> List a dropWhile predicate list = case list of [] -> [] x::xs -> if (predicate x) then dropWhile predicate xs else list
En el anterior ejemplo tenemos las siguientes funciones:
  • dropWhile: función que al tomar una lista elimina los elementos mientras que se cumpla cierta condición (emulación de un while)
  • eratos: Cálculo de la criba de Eratóstenes
  • primes: Función para calcular los primos hasta n
  • primesInRange: Función para calcular los primos en un rango
  • goldbach1: Función para calcular los números primos que solucionan el problema
  • goldbach: Función inicial que llama a goldbach1 solo si los parametros dados son correctos

EJEMPLO PRÁCTICO

El siguiente programa muestra un contador que puede ser aumentado o decrementado por medio de dos botones
module Main exposing (main) import Browser import Html exposing (Html, button, div, text) import Html.Events exposing (onClick) type alias Model = { count : Int } initialModel : Model initialModel = { count = 0 } type Msg = Increment | Decrement update : Msg -> Model -> Model update msg model = case msg of Increment -> { model | count = model.count + 1 } Decrement -> { model | count = model.count - 1 } view : Model -> Html Msg view model = div [] [ button [ onClick Increment ] [ text "+1" ] , div [] [ text <| String.fromInt model.count ] , button [ onClick Decrement ] [ text "-1" ] ] main : Program () Model Msg main = Browser.sandbox { init = initialModel , view = view , update = update }

En el anterior ejemplo podemos ver de forma precisa como se puede alterar un modelo por medio de un mensaje, el cual en este caso es Increment y Decrement, adicionalmente la vista cambia automaticamente al momento que el mensaje es enviado. Para ver el codigo en funcionamiento oprima en el link

REFERENCIAS

Puede consultar mas información en las siguientes fuentes

  • Documentación oficial aquí
  • Programación funcional en Elm aquí
  • Getting Started with the Elm Programming Language aquí
  • 99 Problems solved in Elm aquí
  • Learn you an Elm aquí
  • Functional reactive programming with elm aquí