elm
Programación funcional para interfaces funcionales
Agenda
- ¿Qué es elm? ¿Por qué nos puede interesar?
- Características básicas del lenguaje
- Conceptos de programación funcional en elm
- DOM virtual
- Mailboxes / Addresses /Signals
- Arquitectura básica
- Tasks / Effects / Ports
- Arquitectura ampliada
¿Qué es elm?
Un lenguaje funcional que se compila a JavaScript

Tipado estático con inferencia de tipos
Promesa de eliminar errores en tiempo de ejecución
Muy fácil de interoperar con JavaScript
¿Por que nos puede interesar?
- Tipado: Muchos errores de código se pueden evitar en tiempo de compilación
- Arquitectura basada en principios muy básicos: No hay muchos conceptos que aprender. No hay que elegir entre distintos conceptos como
- controladores
- directivas
- transclude
- services
- factories
- $scope
- etc..
¿Por que nos puede interesar?
- Funciones puras: muuuy fáciles de testear. En caso de una regresión es fácil crear un test que lo reproduzca
- Es fácil interoperar con JavaScript:
- Es posible crear un módulo de Elm que construyamos y agreguemos al DOM
- Es muy fácil escuchar desde JavaScript eventos de un modulo de Elm
- Es muy fácil enviar eventos al módulo desde JavaScript
La mayoría de sistemas de tipos

El sistema de tipos de elm

Mensajes de error amigables


Mensajes de error amigables

Mensajes de error amigables

Mensajes de error amigables
Mensajes de error

Mensajes de error amigables

Mensajes de error amigables

Mensajes de error amigables
Desventajas
- Curva de aprendizaje de programación funcional
- Inmutabilidad
- Higher order functions
- Currying
- Programar orientado a expresiones
- Sum types
- Signals / Tasks / Ports
- Integrar proceso de compilación de elm con el build de la SPA puede ser complicado <- discutible
- Es difícil ver cómo se podría construir una SPA con distintas rutas en elm <- discutible
El lenguaje
Programación funcional y definiciones de tipos
Programación funcional
- Las funciones deben ser "puras": reciben datos y devuelven datos. Una función no puede:
- Modificar estado global
- Modificar sus argumentos
- El objetivo en un lenguaje puramente funcional es combinar expresiones y no describir una secuencia de instrucciones como sucede en un lenguaje imperativo
- Inmutabilidad
Aplicación de funciones
Para invocar una función basta con poner un espacio entre la función y el argumento:
double : Int -> Int
double x = 2 * x
double 3
Funciones anónimas y funciones de orden superior
Las funciones anónimas se declaran con \
doubleList : List Int -> List Int
doubleList xs = List.map (\x -> 2 * x) xs
Orientado a expresiones
absoluteValue : Int -> Int
absoluteValue n = if n > 0
then n
else (-n)
Los ifs son expresiones:
Orientado a expresiones
average : List Double -> Double
average xs = let total = List.sum xs
n = List.length xs
in total / n
Para declarar valores intermedios hay que usar una expresión let in:
Orientado a expresiones
stdDev : List Double -> Double
stdDev xs = let avg = average xs
square x = x * x
diff x = square ( avg - x )
in List.sum (List.map diff xs)
También es posible definir funciones dentro de los let in:
Todas las funciones son de un solo parámetro
add : Int -> (Int -> Int)
add x = (\y -> x + y)
(add 1) 2
Para hacer funciones que reciban múltiples parámetros hay que crear una función que reciba el primer argumento y devuelva una función que reciba el siguiente argumento:
Todas las funciones son de un solo parámetro
add : Int -> (Int -> Int)
Obviamente esto es muy feo.
add : Int -> Int -> Int
Para decir:
Solo hay que decir:
Sin paréntesis las firmas de las funciones asocian a la derecha
Todas las funciones son de un solo parámetro
add3 : Int -> (Int -> (Int -> Int))
add3 : Int -> Int -> Int -> Int
Similarmente:
Solo hay que decir:
Todas las funciones son de un solo parámetro
Por otra parte:
add x = (\y -> x + y)
add x y = x + y
Es lo mismo que:
Todas las funciones son de un solo parámetro
Y:
((add3 4) 5) 6
add3 4 5 6
Es lo mismo que:
Es decir la aplicación de funciones asocia hacia a la izquierda
¿Por que esto?
multiply : Int -> Int -> Int
multiply x y = x * y
doubleList : List Int -> List Int
doubleList xs = List.map (multiply 2) xs
Puede resultar útil para aplicar parcialmente funciones, por ej:
Sum types
type Filter = All
| AssignedTo User
| CreatedBetween Date Date
Un tipo Filter con tres "constructores":
All : Filter
AssignedTo : User -> Filter
CreatedBetween : Date -> Date -> Filter
Pattern matching
applyFilter : Filter -> List Task -> List Task
applyFilter filter tasks =
case filter of
All ->
tasks
AssignedTo user ->
List.filter (assigneeIsEqualTo user) tasks
CreatedBetween start end ->
List.filter (wasCreatedBetween start end) tasks
assigneeIsEqualTo : User -> Task -> Bool
assigneeIsEqualTo user task = ...
wasCreatedBetween : Date -> Date -> Task -> Bool
wasCreatedBetween start end task = ...
Determinar qué alternativa es algo de tipo Filter:
Type alias
type Counter = Int
increment : Counter -> Counter
increment x = x+1
Es posible tener sinónimos de tipos:
Records
type alias Task = { id : String
, description : String
, assignedTo : User
, start : Date
, end : Date
}
Los records son como JSONs tipados e inmutables. Se declaran como type alias:
changeAssignedTo : User -> Task -> Task
changeAssignedTo user task = { task | assignedTo = user }
Para "modificar" un record:
Variables de tipos
always : a -> b -> a
always a = (\_ -> a)
type Tree a = Leaf | Node (Tree a) a (Tree a)
type alias Addable a = { zero : a
, add : a -> a -> a }
Para decir que un tipo es una variable utilizamos un identificador con letras minúsculas.
Signals
Variables que cambian en el tiempo
Signals
Por ejemplo Signal Int representa un flujo de valores enteros que cambia con el tiempo
3
6
4
5
"a"
"ab"
"abc"
"abcd"
1
2
3
4
s1 : Signal String
s1 = ...
s2 : Signal Int
s2 = Signal.map String.length s1
s1
s2
7
2
5
6
2
6
s2 = Signal.filter even s1
s1
s2
3
7
s1
4
5
s2
7
8
12
s3
s3 = Signal.map2 s1 s2 (\a b -> a + b)
3
7
s1
4
5
s2
4
5
7
s3
s3 = Signal.merge s1 s2
3
"xy"
"a"
"rst"
"fg"
2
3
6
8
s2 = Signal.foldp (\s acc -> acc + (String.length s)) 0 s1
s1
s2
0
0+2
2+1
3+3
6+2
Como crear señales
Algunos módulos tienen señales ya construidas o constructores:
-- Keyboard Module
presses : Signal KeyCode
-- The latest key that has been pressed.
-- Mouse Module
position : Signal (Int, Int)
-- The current mouse position.
-- Windows Module
dimensions : Signal (Int, Int)
-- The current width and height of the window (i.e. the
-- area viewable to the user, not including scroll bars).
-- Time Module
every : Time -> Signal Time
-- Takes a time interval t. The resulting signal is the
-- current time, updated every t.
Buzones, direcciones y señales


type alias Mailbox a =
{ address : Address a
, signal : Signal a
}
Address
Signal
mailbox : a -> Mailbox a
{-- Create a mailbox you can
send messages to. The argument
is a default value for the
custom signal. --}
send : Address a -> a -> Task x ()
-- Send a message to an Address.
main
Todo programa de elm que desee mostrar algo en pantalla debe tener un valor llamado main que debe ser de alguno de los siguientes tipos:
- Element
- Html
- Signal Element
- Signal Html
Html es un tipo que representa nodos del DOM. El runtime de elm se encarga de tomar un valor de ese tipo y renderizarlo.
DOM virtual
HTML 1
HTML 2
HTML 3
runtime de elm aplica al DOM real los cambios necesarios para pasar de HTML 1 a HTML 2
runtime de elm aplica al DOM real los cambios necesarios para pasar de HTML 2 a HTML 3
¿Si es un Signal Html no sería ineficiente re-renderizar toda la vista cada vez que haya un cambio?
DOM virtual
div
h1
ul
li
li
div
h1
ul
li
li
li
Ejemplos
import Html exposing (span, text)
import Html.Attributes exposing (class)
main =
span [class "welcome-message"] [text "Hello, World!"]
Ejemplos
import Graphics.Element exposing (..)
import Mouse
main : Signal Element
main =
Signal.map show Mouse.position

module BasicCounter where
import Html exposing (..)
import Html.Events exposing (onClick)
import Signal exposing (Mailbox, mailbox)

¡Hay una mejor forma de organizar esto!
counterHtml : Int -> Html
counterHtml counter =
div [] [ button [ onClick clicksMailbox.address Increment ] [ text "+" ]
, div [] [ text (toString counter) ]
, button [ onClick clicksMailbox.address Decrement ] [ text "-" ] ]
counter : Signal Int
counter =
let applyAction action acc =
case action of
NoOp -> acc
Increment -> acc + 1
Decrement -> acc - 1
in Signal.foldp applyAction 0 clicksMailbox.signal
main = Signal.map counterHtml counter
type Action = Increment
| Decrement
| NoOp
clicksMailbox : Mailbox Action
clicksMailbox = mailbox NoOp
Arquitectura básica de elm
Model View Update
Detrás de la vista hay un modelo, que empieza con cierto estado:
type alias Model = Int
initialModel : Model
initialModel = 0
El usuario puede realizar ciertas acciones que actualizan el modelo:
type Action = Increment
| Decrement
| NoOp
update : Action -> Model -> Model
update : Model -> Html
El modelo determina cómo se verá la aplicación:
type alias Model = Int
initialModel : Int
initialModel = 0
type Action = Increment
| Decrement
| NoOp
update : Action -> Model -> Model
update action model =
case action of
NoOp -> model
Increment -> model + 1
Decrement -> model - 1
view : Model -> Html
view model =
div [] [ button [ onClick actionsMailbox.address Increment ] [ text "+" ]
, div [] [ text (toString model) ]
, button [ onClick actionsMailbox.address Decrement ] [ text "-" ] ]
actionsMailbox : Mailbox Action
actionsMailbox = mailbox NoOp
main = let actions = actionsMailbox.signal -- Signal Action
model = Signal.foldp update initialModel actions -- Signal Model
in Signal.map view model
Model
Update
View
Model
Tipo y estado inicial
View
Función
Update
Función
Pantalla
Teclado /
Mouse
Acciones
Address y Signal
Tomado parcialmente de Unidirectional User Interface Architectures
Model View Update
Paquete start-app . Módulo StartApp.Simple
type alias Config model action =
{ model : model
, view : Address action -> model -> Html
, update : action -> model -> model
}
start : Config model action -> Signal Html
import StartApp.Simple exposing (start)
type alias Model = Int
initialModel : Model
initialModel = 0
type Action = Increment
| Decrement
update : Action -> Model -> Model
update action model =
case action of
Increment -> model + 1
Decrement -> model - 1
view : Address Action -> Model -> Html
view address model =
div [] [ button [ onClick address Increment ] [ text "+" ]
, div [] [ text (toString model) ]
, button [ onClick address Decrement ] [ text "-" ] ]
main = start { update = update, view = view, model = initialModel }
Model
Update
View
Composición
Model
View
Update
Model
View
Update
Model
Update
View
Composición
forwardTo : Address general ->
(especific -> general) ->
Address specific
import Counter
import Signal exposing (Address, forwardTo)
import StartApp.Simple exposing (start)
type alias Model = { first : Counter.Model
, second : Counter.Model }
initialModel : Model
initialModel = { first = Counter.initialModel
, second = Counter.initialModel }

type Action = FirstCounterAction Counter.Action
| SecondCounterAction Counter.Action
update : Action -> Model -> Model
update action model =
case action of
FirstCounterAction counterAction ->
{ model | first = Counter.update counterAction model.first }
SecondCounterAction counterAction ->
{ model | second = Counter.update counterAction model.second }
view : Address Action -> Model -> Html
view address model =
div [] [ b [] [text "first counter: "]
, Counter.view (forwardTo address FirstCounterAction ) model.first
, b [] [text "second counter: "]
, Counter.view (forwardTo address SecondCounterAction) model.second ]
main = start { update = update, view = view, model = initialModel }
Elm
By Miguel Vilá
Elm
- 1,402