.center[twitter & github: @tgroshon]
- Apply Elm's good ideas to our daily work
- Promote Elm as a development tool
- Introduction to Elm
- Details of How it Works
- Type Analysis Advantages
- Signals -> Values over Time
- Abstracting Away Side-Effects
- Sum-up
Some persuasion followed by an Immediate Action (IA)
- 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
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
- Webpack Loader: https://github.com/rtfeldman/elm-webpack-loader
elm-make
JS Bindings: https://github.com/rtfeldman/node-elm-compiler
- Advantages of Elm's Type System
- Catch Simple Errors
- Banish Nulls
- Enforce Semantic Versioning
- IA: Flow
Goodbye spelling mistakes and refactoring lapses:
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.
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
.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/
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
As a habit in flow, mark things as Maybe
- Discrete values over time
- Signal Graphs
- IA: RxJS, Most.js, Flyd
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
- Synchronous: FIFO
- Infinite & Static: You cannot delete, create, or unsubscribe from signals at runtime.
Your program ends up being a graph of signals.
Think of pipes (streams): (a) sources, (b) branches, and (c) sinks.
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
- Naturally handles asynchrony
- Naturally model data over time while avoiding shared mutable state
Contenders:
- RxJS (Biggest)
- Bacon.js (Slowest)
- Kefir.js (Middle)
- Most.js (Fastest)
- Flyd (Smallest)
I'll discuss RxJS, Most, and Flyd
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
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.
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
- Tasks
- Effects
- IA: Promises
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
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
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 aMaybe String
type.Task.map NewGif
wraps theMaybe String
in aNewGif
action.Effects.task
turn this task into anEffects
type.
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))
Thoughts?
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.
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})
})
})
- 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 ;)