Skip to content

Latest commit

 

History

History
471 lines (319 loc) · 9.24 KB

README.md

File metadata and controls

471 lines (319 loc) · 9.24 KB

What is Elm and Why Should I Care?

.center[twitter & github: @tgroshon]


Direct Complaints as Follows

trash can


Goals

  1. Apply Elm's good ideas to our daily work
  2. Promote Elm as a development tool

Agenda

  1. Introduction to Elm
  2. Details of How it Works
  3. Type Analysis Advantages
  4. Signals -> Values over Time
  5. Abstracting Away Side-Effects
  6. Sum-up

Format

Some persuasion followed by an Immediate Action (IA)


Intro: Syntax

  • Haskell-like syntax
  • Statically Typed
  • Declarative
  • Compile-to-JavaScript
import Graphics.Element exposing (..)
import Window

main : Signal Element
main =
  Signal.map resizeablePaint Window.dimensions

resizeablePaint : (Int,Int) -> Element
resizeablePaint (w,h) =
  fittedImage w h "/imgs/paint.jpg"

-- Types can be ommitted/inferred in most cases but are nice for Doc

Type Aliases

type alias Name = String
type alias Age = Int

Intro: Tool-chain

Written in Haskell

elm-make       # Compiles Elm to JS
elm-package    # Package Manager
elm-repl       # A Read-Eval-Print-Loop
elm-reactor    # Quick-start Watcher/Builder
elm            # Wrapper around all the tools

Other tools


The Type Analysis Advantage

This just in: Dynamic types are out, Static types are in!

  • Advantages of Elm's Type System
    • Catch Simple Errors
    • Banish Nulls
    • Enforce Semantic Versioning
  • IA: Flow

The Simple Errors

Goodbye spelling mistakes and refactoring lapses:

Elm error messages


Banish Null

I call it my billion-dollar mistake. It was the invention of the null reference in 1965. - Tony Hoare (Builder of Algol W)

No nulls in Elm, Just Maybes:

type Maybe a = Just a | Nothing

Allows the compiler to enforce "null-checks" when needed.


Enforcing Semantic Versioning

elm-package will bump versions for you, automatically enforcing these rules:

  • all packages start with initial version 1.0.0
  • Versions are incremented based on how the API changes:
    • PATCH - the API is the same, no risk of breaking code
    • MINOR - values have been added, existing values are unchanged
    • MAJOR - existing values have been changed or removed

Even Rust-lang is talking about adding similar checks: https://users.rust-lang.org/t/signature-based-api-comparison/2377


IA: Flow

.center[flowtype.org]

/* @flow */
function foo(x) {
  return x * 10;
}
foo('Hello, world!');  // Error: This type is incompatible with ...: string
/* @flow */
function foo(x: string, y: number): string {
  return x.length * y;
}
foo('Hello', 42); // Error:  number ... type is incompatible with ... string

Babel will strip it out for you: http://babeljs.io/docs/plugins/transform-flow-strip-types/


One Beef with Flow

In JavaScript, null implicitly converts to all the primitive types; it is also a valid inhabitant of any object type.

In contrast, Flow considers null to be a distinct value that is not part of any other type.

  • Let's be Honest: In JavaScript, all types are nullable!
  • Requires programmer discipline to be honest about which vars are not Maybe (fewer than we think)
  • Not a great decision for a gradually-typed system over a language with nullable types

Recommendation

As a habit in flow, mark things as Maybe


Signals -> Values Over Time

  • Discrete values over time
  • Signal Graphs
  • IA: RxJS, Most.js, Flyd

Discrete Values Over Time

type Signal a

A value that changes over time. Every signal is updated at discrete moments in response to events in the world.

So like an EventEmitter? Almost, and not quite.

The Reactive in Elm Functional-Reactive Programming


Signal Characteristics

  • Synchronous: FIFO
  • Infinite & Static: You cannot delete, create, or unsubscribe from signals at runtime.

Signal Graph

Your program ends up being a graph of signals.

Think of pipes (streams): (a) sources, (b) branches, and (c) sinks.

Elm signal Diagram


Signal Graph (cont'd)

An Application's signal graph can have multiple sources, branches, and sinks.

Sources:

position : Signal (Int, Int)   -- Mouse
clicks : Signal ()             --

dimensions : Signal (Int, Int) -- Window

Branches (Transforms):

map : (a -> b) -> Signal a -> Signal b
filter : (a -> Bool) -> a -> Signal a -> Signal a
merge : Signal a -> Signal a -> Signal a
foldp : (a -> s -> s) -> s -> Signal a -> Signal s

Sinks:

main : Signal Element                 -- element to be rendered to screen
port someJavaScriptFn : Signal String -- Send strings to JavaScript

But Why Signals?

  1. Naturally handles asynchrony
  2. Naturally model data over time while avoiding shared mutable state

IA: FRP Streams in JS

Contenders:

  • RxJS (Biggest)
  • Bacon.js (Slowest)
  • Kefir.js (Middle)
  • Most.js (Fastest)
  • Flyd (Smallest)

I'll discuss RxJS, Most, and Flyd


IA: RxJS

RxJS implements Observables

/* Get stock data somehow */
var source = getAsyncStockData()    // Source is an observable

source
  .filter(function (quote) {        // Branches
    return quote.price > 30
  })
  .map(function (quote) {
    return quote.price
  })
  .subscribe(function (price) {     // Sink
    console.log('Prices higher ' +
                'than $30: $' +
                price)
  })

Most popular but also the largest file size (around 200kb minified)

Can choose asynchrony between setImmediate, setTimeout, requestAnimationFrame


IA: Most.js

Monadic Streams

most.from([1, 2, 3, 4])
  .delay(1000)
  .filter(function(i) {
    return i % 2 === 0
  })
  .reduce(function(result, y) {
    return result + y
  }, 0)
  .then(function(result) {
    console.log(result)
  })

Super fast and around 50kb minified, uncompressed

Consuming functions (reduce, forEach, observe) return Promises.


IA: Flyd

Pronounced flu (it's Dutch)

var x = flyd.stream(10) // Create stream with initial value
var y = flyd.stream(20)

var sum = flyd.combine(function(x, y) {
  return x() + y() // Calling stream without argument returns last value
}, [x, y])

flyd.on(function(s) {
  console.log(s)
}, sum)

Very Simple. Modeled after Elm Signals.

Mostly just gives low-level composition constructs e.g. combine, merge, transduce


Abstracting Away Side-Effects

  • Tasks
  • Effects
  • IA: Promises

Tasks

type Task x a

Represents asynchronous effects that return value of type a but may fail with value of type x.

Sound like Promises? They are even chainable with andThen:

logCurrentTime : Task x ()
logCurrentTime =
  getCurrentTimeTask `andThen` consoleLogTask

Effects

type Effects a

The Effects type is essentially a data structure holding a bunch of independent tasks that will get run at some later point.

Example making an HTTP request for jsonData:

getData : String -> Effects Action
getData url =
  Http.get decodeJsonData url
    |> Task.toMaybe
    |> Task.map NewGif
    |> Effects.task

Explained

type Action = NewGif (Maybe String) | ... -- some other action

getData : String -> Effects Action
getData url =
  Http.get decodeJsonData url
    |> Task.toMaybe
    |> Task.map NewGif
    |> Effects.task
  • Task.toMaybe drops the error message and returns a Maybe String type.
  • Task.map NewGif wraps the Maybe String in a NewGif action.
  • Effects.task turn this task into an Effects type.

Task, Maybe, Effects Oh My!

Effects wrap Tasks that are guaranteed to "not fail" ... meaning they have an error handler.

Task.toMaybe and Task.toResult are convenience functions to move errors into the success handler by returning a new task.

toMaybe : Task x a -> Task y (Maybe a)
toMaybe task =
  map Just task `onError` (\_ -> succeed Nothing)

toResult : Task x a -> Task y (Result x a)
toResult task =
  map Ok task `onError` (\msg -> succeed (Err msg))

Why are Effects useful?

Thoughts?


Why are Effects useful?

My theory: to make the start-app package better!

If you can collect the tasks for all your components that will be rendered, you can run them in a batch but only if they never fail.


IA: Promises

var p = new Promise(function(resolve, reject) {
  // something async happens
});

p.then(successHandlerFn, failureHandlerFn);

What can we learn from Elm?

Don't reject promises needlessly

var p = new Promise(function (resolve, reject) {
  doAsync(function (err, result) {
    if (err) {
      return resolve({status: 'failed', data: err})
    }
    resolve({status: 'success', data: result})
  })
})

Sum-up

  • Try out Elm
  • Or:
    • Use flow for static type analysis
    • Use a JavaScript FRP Library
    • Use Promises that "never fail"

More realistically, use some combination of the above ;)