Practical Elm. And friends

Making Albatross

Peter Szerzo

Cph Frontenders, July 12th, 2016

Hi, I'm Peter

I write front-end at Airtame.

I go by @pickled-plugins on GitHub, @peterszerzo on Twitter. I'm online on peterszerzo.com.

I like side projects

Albatross is one of them.

It's an interactive map that hosts and documents everyday sounds. A sound diary, if you will.

It's on GitHub.

Making side projects enjoyable

It's our fun time

...for heaven's sake.

There's little help

No code reviews. No white-board discussions.

Pieces of advice to myself

  • make friends with your process.
  • use tools you love, and use them naturally.
  • find a way to not have to spend hours debugging.
  • take tiny steps. Start with a cute little front-end, maybe.

And so...

Meet Elm - in conversation

The module name is messed up for ././src/Main/Messages.elm

    According to the file's name it should be Main.Messages
    According to the source code it should be Main.Messagess

Which is it?

Meet Elm - more chit-chat

This `case` does not have branches for all possibilities.

case msg of
  WaterYourPlants -> "indeed"
  TalkAboutYourFeelings -> "always"

You need to account for the following values:

  HaveAGreatTime

Add a branch to cover this pattern!

Elm in a nutshell

  • purely functional language: no objects, not even for loops, juuust some functions.
  • brilliant type system - always know what you get.
  • no runtime errors. If you find one, just file it as a compiler bug.
  • a rock-solid package manager that yells at you if a change in a package's API breaks semantic versioning.
  • a real clever way to structure front-end code, one that emerges naturally from the design of the language (just parroting back what I heard at a talk).
  • built with the developer in mind. Pleasantness all over.

Resources

Elm

JavaScript, Elm-style

How Albatross is built

Elm brain

  • hangs on to the state of the UI (model).
  • defines all ways it can change (messages), and the changes themselves (updates).
  • renders the core of the UI (view), wires up event listeners to these ways of changes.
  • subscribes to stuff (time, websockets, outside JavaScript).
  • runs commands (networks requests, messages to outside JavaScript).
  • outsources all the stuff it cannot do.

How Albatross is built

JavaScript services

  • our not very bright, but faithful worker bees: just draw a map, just reload an audio tag.
  • little or no if, try/catch (they're awkward).
  • no fetch or promises (they bite and cause nightmares).
  • no typeof a === 'undefined' || a === null.
  • just some very clear messages from Elm and very clear messages back into Elm.

Why?

Elm

  • responsible, expressive, safe, proper.
  • structuring and restructuring stuff is a breeze (stay tuned for demo).
  • sometimes, though, it's just nice to do a $('.everything').fadeOut(). Elm says no can do!

JavaScript

  • gives a whole lot of freedom to interact with third-party libraries.
  • the same freedom makes me crash and burn as soon as logic and error handling comes into play.

Who's more trustworthy?

function greetFirst(names) {
  var first = names[0];
  if (typeof first !== 'undefined' && first !== null) {
    return 'Hello, ' + first;
  }
  return 'Umm, your name again?';
}

Who's more trustworthy?

greetFirst : List String -> String
greetFirst names =
  List.head names
    |> Maybe.map (\name -> "Hello, " ++ name)
    |> Maybe.withDefault "Um, your name again?"

Onto some Albatross code snippets...

Elm brain - models

type Route = Home | About

type alias Sound =
  { id : String
  , title : String
  , trackUrl : String
  , lat : Float
  , lng : Float
  }

Elm brain - main UI model

type alias Model =
  { route : Route
  , sounds : List Sound
  , activeSoundId : Maybe String
  , isMapReady : Bool
  }

Elm brain - init

A function that returns two things:

  • the initial ui state.
  • some commands that run at the beginning of the program (more on this in a bit).
init : (Model, Cmd Msg)
init =
  ( Model Home [] Nothing False
  , Cmd.batch [fetchSoundsCommand, createMapCommand]
  )

Elm Brain - messages

A message describes what can possibly happen with this UI.

type Msg =
  ChangeRoute |
  MapReady Bool |
  SetActiveSound String |
  ClearActiveSound |
  FetchSoundsFail Http.Error |
  FetchSoundsSucceed (List Sound)

Elm Brain - update

The update function takes a message and the old state of the UI as arguments. It returns two things (like before):

  • the new model.
  • a command (e.g. http) that sends another message when completed.
update msg model =
  case msg of
    SetActiveSoundId id ->
      ({model | activeSoundId = id}, reloadAudioCommand)
    ...

Elm brain - subscriptions

The Elm program subscribes to events coming from the outside world (the time, websockets, messages from JS code).

subscriptions model =
  Sub.batch
  [ mapReady MapReady
  , setActiveSound SetActiveSound
  , clearActiveSound ClearActiveSound
  ]

Elm brain - view

Renders Html, wires up event handlers to messages.

view model =
  div [class "wrapper", onClick ChangeRoute] [text "Click Me!"]

Elm brain - wiring

This is it!

main =
  program
  { init = init
  , update = update
  , view = view
  , subscriptions = subscriptions
  }

Elm brain - finally!

var domReady = require('domready');
var Elm = require('./Main.elm');

domReady(function() {
  console.log('Hi, Mom!');
  var elmApp = Elm.Main.embed(document.getElementById('elm-app'));
});

JS services - the mapper

function createMap(handlers) {
  var map = new mapboxgl.Map({container: 'app', style: 'mf'});
  map.on('load', handlers.onCreated);
  map.on('click', function(e) {
    // Not asking questions, just calling the boss...
    handlers.onClick(e.lngLat.lat, e.lngLat.lng);
  });
}

JS services - hooking it up!

domReady(function() {
  console.log('Hi, Mom!');
  var elmApp = Elm.Main.embed(document.getElementById('elm-app'));
  elmApp.ports.createMap.subscribe(function() {
    createMap({
      onCreated: function() {
        elmApp.ports.mapReady.send(true);
      }
    });
  });
}

Meanwhile, in the brain...

port mapReady : (Bool -> msg) -> Sub msg

subscriptions model = mapReady MapReady

update msg model =
  case msg of
    MapReady ->
      ({model | isMapReady = True}, renderSoundsCommand)
  ...

In a nutshell

  • set up a robust core of the application in Elm.
  • set up some very dump and decoupled JavaScript modules for stuff Elm cannot do.
  • wire them up with crystal-clear messages, subscriptions and commands.

Can I use this at work?

Yes. Eventually.

But I can write Elm-style JavaScript tomorrow.

Let's write some Elm!

Oh, and here are my slides: https://pickled-plugins.github.io/practical-elm-and-friends/

If you liked them, might you want to consider giving me feedback on my slide generator?