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

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 }
Made with Slides.com