Real World Elm

Just enough to build complex Elm applications

How a pure functional language interacts with the “world”?

Tech Lead @ Meetic

k.lebrun@meetic-corp.com

kevinlebrun

Signals

Event based / Infinite

Simple signals

Transform

position : Signal (Int, Int)
position =
    Signal.map2 (,) Mouse.x Mouse.y

Transform

type Action
    = NoOp
    | Pause
    | Resume

keypresses : Signal Action
keypresses =
    let
        handleKeyboard : KeyCode -> Action
        handleKeyboard code =
            let
                key = fromCode code
            in
                if key == 'P' || key == 'p' then
                   Pause
                else if key == 'R' || key == 'r' then
                   Resume
                else
                   NoOp

    in
        Signal.map handleKeyboard Keyboard.presses

Merge

keypresses : Signal Action
keypresses = ...

points : Signal Action
points = ...

inputs : Signal Action
inputs = Signal.mergeMany
    [ points
    , keypresses
    ]

Manage state

model : Signal Model
model =
    let
        inputs = Signal.mergeMany
            [ points
            , keypresses
            ]
    in
        Signal.foldp update initialModel inputs

Filter

dropNoOp = 
    Signal.filter ((/=) NoOp) NoOp keypresses

Mailbox

-- create a new mailbox with a default value
actions : Signal.Mailbox Action
actions =
    Signal.mailbox NoOp

-- compute a new model after we receive 
-- a new message in the mailbox (via signal)
model : Signal Model
model =
    Signal.foldp update canvas actions.signal

-- pass the mailbox address to the view
main : Signal Html
main =
    Signal.map (view actions.address) model

-- dispatch a new action into the mailbox address
view : Signal.Address Action -> Model -> Html
view address model =
    div [ class "container" ]
        [ button [ onClick address Pause ] [ text "Pause" ]
        , button [ onClick address Resume ] [ text "Resume" ]
        ]

Delegation

-- forward the view address to delegate
viewCounter : Signal.Address Action -> (ID, Counter.Model) -> Html
viewCounter address (id, model) =
  Counter.view (Signal.forwardTo address (Modify id)) model



-- delegate model update
type Action = Modify ID Counter.Action

update : Action -> Model -> Model
update action model =
  case action of
    Modify id counterAction ->
      let updateCounter (counterID, counterModel) =
              if counterID == id then
                  (counterID, Counter.update counterAction counterModel)
              else
                  (counterID, counterModel)
      in
          { model | counters = List.map updateCounter model.counters }

Debounce

debounce : Time.Time -> Signal a -> Signal a
debounce wait signal =
    Signal.sampleOn (
        Time.since wait signal
            |> Signal.dropRepeats
            |> Signal.filter ((==) False) False
        ) signal

Debug

debounce : Time.Time -> Signal a -> Signal a
debounce wait signal =
    Signal.sampleOn (
        Time.since wait signal
            |> Signal.map (Debug.log "dropped") 
                << Signal.dropRepeats
            |> Signal.map (Debug.log "filtered") 
                << Signal.filter ((==) False) False
        ) signal

Ports

How Elm interacts with JavaScript?

Incoming ports

port incomingPoints : Signal Point
var div = document.getQuerySelector('.component');
var myapp = Elm.embed(Elm.Main, div);

myapp.ports.incomingPoints.send([0, 0]);
myapp.ports.incomingPoints.send([50, 50]);

Outgoing ports

port outgoingPoints : Signal Point
port outgoingPoints =
    points
myapp.ports.outgoingPoints.subscribe(function (point) {
    console.log(point);
});

LocalStorage

myapp.ports.savePoints.subscribe(function (point) {
    localStorage.setItem(point.id, JSON.stringify(point));
});
port savePoint : Signal Point
port savePoint = 
    points

Ask for confirmation

port getDeleteConfirmation : Signal Int

port askDeleteConfirmation : Signal Point
port askDeleteConfirmation =
  Signal.filterMap extractDeletionIntent actions.signal
app.ports.askDeleteConfirmation.subscribe(function (args) {
  console.log('askDeleteConfirmation', args);
  var id = args[0];
  var message = args[1];
  var response = window.confirm(message);
  if (response) {
    app.ports.getDeleteConfirmation.send(id);
  }
})

Tasks

Communicate with the “world”

A simple task

fetchEmail : String -> Task Http.Error String
fetchEmail username =
    Http.get decodeEmail ("https://api.github.com/users/" ++ username)

decodeEmail : Decoder String
decodeEmail =
    at ["email"] string

Transform

fetchEmail : String -> Task Http.Error String
fetchEmail username =
    Http.get decodeEmail ("https://api.github.com/users/" ++ username)


fetchFullName : String -> Task Http.Error String
fetchFullName username =
    Task.map2 (++) (fetchFirstName username) (fetchLastName username) 


fetchEmailMaybe : String -> Task x (Maybe String)
fetchEmailMaybe username =
    fetchEmail username
        |> Task.toMaybe


fetchEmailResult : String -> Task x (Result Http.Error String)
fetchEmailResult username =
    fetchEmail username
        |> Task.toResult

Chain

fetchEmailThenUpdate : Signal Task x ()
fetchEmailThenUpdate username =
    fetchEmail username 
        `andThen` (\email -> Signal.send actions.address (OnEmailReceived email))
     -- `andThen` (OnEmailReceived << Signal.send actions.address)

Execute a task

port runner : Signal (Task x ())
port runner =
    fetchEmailThenUpdate query.signal
port runner : Signal (Task x ())
port runner =
    let
        extractQuery action =
            case action of
                UpdateQuery query -> 
                    Just query

                _ -> 
                    Nothing

        query = debounce 500 <| Signal.filterMap extractQuery "" actions.signal

    in
        Signal.map fetchEmailThenUpdate query

How a pure functional language interacts with the “world”?

How to start?

Q/A

k.lebrun@meetic-corp.com

kevinlebrun

Real World Elm

By Kevin Le Brun

Real World Elm

  • 2,640