elm: give it a try
Eugene Zharkov, JUNO
the best of functional programming
in your browser
So Elm…
readItem :: String -> Maybe Decl
readItem x | ParseOk y <- myParseDecl x = Just $ unGADT y
readItem x -- newtype
| Just x <- stripPrefix "newtype " x
, ParseOk (DataDecl a _ c d e f g) <- fmap unGADT $ myParseDecl $ "data " ++ x
= Just $ DataDecl a NewType c d e f g
readItem x -- constructors
| ParseOk (GDataDecl _ _ _ _ _ _ [GadtDecl s name _ ty] _) <- myParseDecl $ "data
Data where " ++ x
, let f (TyBang _ (TyParen x@TyApp{})) = x
f (TyBang _ x) = x
f x = x
parserC warning = f [] ""
where
f com url = do
x <- await
whenJust x $ (i,s) -> case () of
_ | Just s <- strStripPrefix "-- | " s -> f [s] url
| Just s <- strStripPrefix "--" s -> f (if null com then [] else
strTrimStart s : com) url
| Just s <- strStripPrefix "@url " s -> f com (strUnpack s)
| strNull $ strTrimStart s -> f [] ""
| otherwise -> do
case parseLine $ fixLine $ strUnpack s of
Left y -> lift $ warning $ show i ++ ":" ++ y
-- only check Nothing as some items (e.g. "instance () :>
Foo a")
-- don't roundtrip but do come out equivalent
It was Haskell
Real Elm
results : Signal.Mailbox (Result String (List String))
results =
Signal.mailbox (Err "A valid US zip code is 5 numbers.")
port requests : Signal (Task x ())
port requests =
Signal.map lookupZipCode query.signal
|> Signal.map (task -> Task.toResult task `andThen` Signal.send results.address)
lookupZipCode : String -> Task String (List String)
lookupZipCode query =
let toUrl =
if String.length query == 5 && String.all Char.isDigit query
then succeed ("http://api.zippopotam.us/us/" ++ query)
else fail "Give me a valid US zip code!"
in
toUrl `andThen` (mapError (always "Not found :(") << Http.get places)
cene : (Int,Int) -> (Int,Int) -> Element
scene (x,y) (w,h) =
let
(dx,dy) =
(toFloat x - toFloat w / 2, toFloat h / 2 - toFloat y)
in
collage w h
[ ngon 3 100
|> filled blue
|> rotate (atan2 dy dx)
, ngon 6 30
|> filled orange
|> move (dx, dy)
Installation
npm install -g elm
elm-compiler
compiler, yep
elm-make
build tools
compile to JS or HTML
build dependencies
elm-reactor
dev tools
hot swap
time travel debugging
elm-repl
ets you interact with values and functions directly.
elm-package
package manager
JS > Elm | diff
string "functional" "functional"
multiline non exists """functional programming"""
string
char non exists 'f'
bool true/false True/False
> Literals
{ width: 300, height: 400} { width = 300, height = 400}
worker.id = 12 { worker | id = 12 }
> Objects aka Records
function(x,y) { return x + y; } x y -> x + y
Math.max(3, 4) max 3 4
Math.min(1, Math.pow(2, 4)) min 1 (2^4)
numbers.map(Math.sqrt) List.map sqrt numbers
points.map(function(p) { return p.x }) List.map .x points
> Functions
JS > Elm | diff
'NCC-' + '1701' "NCC-" ++ "1701"
'NCC-'.length String.length "NCC-"
'NCC-'.toLowerCase() String.toLower "NCC-"
'NCC-' + 1701 "NCC-" ++ toString 1701
> Work with strings
2 > 1 ? 'Lee' : 'Chan' if 3 > 2 then "Lee" else "Chan"
var x = 42; let x = 42
return 42 Everything is an expression, no need for return
> Control Flow
Core
> 9 / 2
4.5 : Float
> 9 // 2
4 : Int
>
> Values
> isNegative n = n < 0
<function>
> isNegative 4
False
> isNegative -7
True
> isNegative (-3 * -4)
False
> Functions
Data Structures
> Lists
> names = [ "Alice", "Bob", "Chuck" ]
["Alice","Bob","Chuck"]
> List.isEmpty names
False
> List.length names
3
> List.reverse names
["Chuck","Bob","Alice"]
> numbers = [1,4,3,2]
[1,4,3,2]
> List.sort numbers
[1,2,3,4]
> double n = n * 2
<function>
> List.map double numbers
[2,8,6,4]
List != Object

elm != OOP
List = Module
List module
module List
( isEmpty, length, reverse, member
, head, tail, filter, take, drop
, repeat, (::), append, concat, intersperse
, partition, unzip
, map, map2, map3, map4, map5
, filterMap, concatMap, indexedMap
, ……………
{-| Determine if a list is empty.
isEmpty [] == True
-}
isEmpty : List a -> Bool
isEmpty xs =
case xs of
[] ->
True
_ ->
False
{-| Keep only elements that satisfy the
predicate.
filter isEven [1..6] == [2,4,6]
-}
filter : (a -> Bool) -> List a -> List a
filter pred xs =
let
conditionalCons x xs' =
if pred x then
x :: xs'
else
xs'
in
foldr conditionalCons [] xs
Data Structures
> Tuples
> import String
> goodName name = 
| if String.length name <= 20 then 
| (True, "name accepted!") 
| else 
| (False, "name was too long; please limit it to 20
characters")
> goodName "Tom"
(True, "name accepted!")
Data Structures
> Records
> point = { x = 3, y = 4 }
{ x = 3, y = 4 }
> point.x
3
> bill = { name = "Gates", age = 57 }
{ age = 57, name = "Gates" }
> bill.name
“Gates"
> .name bill
"Gates"
> List.map .name [bill,bill,bill]
["Gates","Gates","Gates"]
Data Structures
> Records
> under70 {age} = age < 70
<function>
> under70 bill
True
> under70 { species = "Triceratops", age = 68000000 }
False
> { bill | name = "Nye" }
{ age = 57, name = "Nye" }
> { bill | age = 22 }
{ age = 22, name = "Gates" }
JS Object vs Elm Record
- You cannot ask for a field that does not exist.
- No field will ever be undefined or null.
- You cannot create recursive records with a this or self keyword.
Contracts
import String
fortyTwo : Int
fortyTwo = 42
drinks : List String
drinks = [ "Hennessy", "JimBeam", "Jack Daniels" ]
book : { title: String, author: String, pages: Int }
book =
{ title = "Robert", author = "Martin", pages = 434 }
longestName : List String -> Int
longestName drinks =
List.maximum (List.map String.length drinks)
isLong : { record | pages : Int } -> Bool
isLong book =
book.pages > 400
All of these types can be inferred, so you
can leave off the type annotations and Elm
can still check that data is flowing around in
a way that works. This means you can just
not write these contracts and still get all the
benefits!
Enumerations
type Visibility = All | Active | Completed
toString : Visibility -> String
toString visibility =
case visibility of
All ->
"All"
Active ->
"Active"
Completed ->
"Completed"
-- toString All == "All"
-- toString Active == "Active"
-- toString Completed == "Completed"
State Machines
type User = Anonymous | LoggedIn String
userPhoto : User -> String
userPhoto user =
case user of
Anonymous ->
"anon.png"
LoggedIn name ->
"users/" ++ name ++ "/photo.png"
Architecture
Model
View
Action
MUV
-- MODEL
type alias Model = { ... }
-- UPDATE
type Action = Reset | ...
update : Action -> Model -> Model
update action model =
case action of
Reset -> ...
...
-- VIEW
view : Model -> Html
view =
...
Example
type alias Model =
{ topic : String
, gifUrl : String
}
init : String -> (Model, Effects Action)
init topic =
( Model topic "assets/waiting.gif"
, ge
update : Action -> Model -> (Model, Effects Action)
update action model =
case action of
RequestMore ->
(model, getRandomGif model.topic)
NewGif maybeUrl ->
( Model model.topic (Maybe.withDefault model.gifUrl maybeUrl)
, Effects.none
)
Model
Update
Example
getRandomGif : String -> Effects Action
getRandomGif topic =
Http.get decodeUrl (randomUrl topic)
|> Task.toMaybe
|> Task.map NewGif
|> Effects.task
randomUrl : String -> String
randomUrl topic =
Http.url "http://api.giphy.com/v1/gifs/random"
[ "api_key" => "dc6zaTOxFJmzC"
, "tag" => topic
]
decodeUrl : Json.Decoder String
decodeUrl =
Json.at ["data", "image_url"] Json.string
Effects
Functional stuff, I got it
App flow
Signals
Signals
module Signal
( Signal
, merge, mergeMany
, map, map2, map3, map4, map5
, constant
, dropRepeats, filter, filterMap, sampleOn
, foldp
, Mailbox, Address, Message
, mailbox, send, message, forwardTo
) where
Tasks
elm-http — talk to servers
elm-history — navigate browser history
elm-storage — save info in the users browser
elm-storage
import Native.Storage
getItemAsJson : String -> Task String Value
getItemAsJson = Native.Storage.getItemAsJson
-- Do better error detection
getItem : String -> Decoder value -> Task String value
getItem key decoder =
let decode value = case decodeValue decoder value of
Ok v -> succeed v
Err err -> fail "Failed"
in
getItemAsJson key `andThen` decode
Native.Storage aka Storage.js
/*!
localForage -- Offline Storage, Improved
Version 1.2.2
https://mozilla.github.io/localForage
(c) 2013-2015 Mozilla, Apache License 2.0
*/
(function() {
var define, requireModule, require, requirejs;
(function() {
var registry = {}, seen = {};
define = function(name, deps, callback) {
registry[name] = { deps: deps, callback: callback };
};
requirejs = require = requireModule = function(name) {
requirejs._eak_seen = registry;
if (seen[name]) { return seen[name]; }
seen[name] = {};
Mailbox
type alias Mailbox a =
{ signal : Signal a
, address : Address a
}
send : Address a -> a -> Task x ()
import Graphics.Element exposing (Element, show)
import Task exposing (Task, andThen)
import TaskTutorial exposing (getCurrentTime, print)
main : Signal Element
main =
Signal.map show contentMailbox.signal
contentMailbox : Signal.Mailbox String
contentMailbox =
Signal.mailbox ""
port updateContent : Task x ()
port updateContent =
Signal.send contentMailbox.address "hello!"
Init with empty value
send a message
signature
Ports - Communication between Elm and
pure JS
Ports | JS > Elm
port addUser : Signal (String, UserRecord)
> Elm
> JS
myapp.ports.addUser.send([
"Tom",
{ age: 32, job: "lumberjack" }
]);
myapp.ports.addUser.send([
"Sue",
{ age: 37, job: "accountant" }
]);
Ports | Elm > JS
port requestUser : Signal String
port requestUser =
signalOfUsersWeWantMoreInfoOn
> Elm
> JS
myapp.ports.requestUser.subscribe(databaseLookup);
function databaseLookup(user) {
var userInfo = database.lookup(user);
myapp.ports.addUser.send(user, userInfo);
}
HTML generation
main : Signal Element
main =
Signal.map2 renderStamps Window.dimensions clickLocations
clickLocations : Signal (List (Int,Int))
clickLocations =
Signal.foldp (::) [] (Signal.sampleOn Mouse.clicks Mouse.position)
renderStamps : (Int,Int) -> List (Int,Int) -> Element
renderStamps (w,h) locs =
let pentagon (x,y) =
ngon 5 20
|> filled (hsla (toFloat x) 0.9 0.6 0.7)
|> move (toFloat x - toFloat w / 2, toFloat h / 2 - toFloat y)
|> rotate (toFloat x)
in
layers
[ collage w h (List.map pentagon locs)
, show "Click to stamp a pentagon."
]
elm-make Stamper.elm --output=Main.html
Nice architecture
Good documentation
Source code with comments
Great developer toolset
Own packages / manager
Overview from JS perspective
Complex syntax
Learning curve
Team integration cost?
Overview from JS perspective
That's all folks
eu.zharkov@gmail.com
@2j2e

Elm: give it a try

  • 1.
    elm: give ita try Eugene Zharkov, JUNO
  • 2.
    the best offunctional programming in your browser
  • 3.
    So Elm… readItem ::String -> Maybe Decl readItem x | ParseOk y <- myParseDecl x = Just $ unGADT y readItem x -- newtype | Just x <- stripPrefix "newtype " x , ParseOk (DataDecl a _ c d e f g) <- fmap unGADT $ myParseDecl $ "data " ++ x = Just $ DataDecl a NewType c d e f g readItem x -- constructors | ParseOk (GDataDecl _ _ _ _ _ _ [GadtDecl s name _ ty] _) <- myParseDecl $ "data Data where " ++ x , let f (TyBang _ (TyParen x@TyApp{})) = x f (TyBang _ x) = x f x = x parserC warning = f [] "" where f com url = do x <- await whenJust x $ (i,s) -> case () of _ | Just s <- strStripPrefix "-- | " s -> f [s] url | Just s <- strStripPrefix "--" s -> f (if null com then [] else strTrimStart s : com) url | Just s <- strStripPrefix "@url " s -> f com (strUnpack s) | strNull $ strTrimStart s -> f [] "" | otherwise -> do case parseLine $ fixLine $ strUnpack s of Left y -> lift $ warning $ show i ++ ":" ++ y -- only check Nothing as some items (e.g. "instance () :> Foo a") -- don't roundtrip but do come out equivalent
  • 4.
  • 5.
    Real Elm results :Signal.Mailbox (Result String (List String)) results = Signal.mailbox (Err "A valid US zip code is 5 numbers.") port requests : Signal (Task x ()) port requests = Signal.map lookupZipCode query.signal |> Signal.map (task -> Task.toResult task `andThen` Signal.send results.address) lookupZipCode : String -> Task String (List String) lookupZipCode query = let toUrl = if String.length query == 5 && String.all Char.isDigit query then succeed ("http://api.zippopotam.us/us/" ++ query) else fail "Give me a valid US zip code!" in toUrl `andThen` (mapError (always "Not found :(") << Http.get places) cene : (Int,Int) -> (Int,Int) -> Element scene (x,y) (w,h) = let (dx,dy) = (toFloat x - toFloat w / 2, toFloat h / 2 - toFloat y) in collage w h [ ngon 3 100 |> filled blue |> rotate (atan2 dy dx) , ngon 6 30 |> filled orange |> move (dx, dy)
  • 6.
    Installation npm install -g elm elm-compiler compiler, yep elm-make build tools compileto JS or HTML build dependencies elm-reactor dev tools hot swap time travel debugging elm-repl ets you interact with values and functions directly. elm-package package manager
  • 7.
    JS > Elm| diff string "functional" "functional" multiline non exists """functional programming""" string char non exists 'f' bool true/false True/False > Literals { width: 300, height: 400} { width = 300, height = 400} worker.id = 12 { worker | id = 12 } > Objects aka Records function(x,y) { return x + y; } x y -> x + y Math.max(3, 4) max 3 4 Math.min(1, Math.pow(2, 4)) min 1 (2^4) numbers.map(Math.sqrt) List.map sqrt numbers points.map(function(p) { return p.x }) List.map .x points > Functions
  • 8.
    JS > Elm| diff 'NCC-' + '1701' "NCC-" ++ "1701" 'NCC-'.length String.length "NCC-" 'NCC-'.toLowerCase() String.toLower "NCC-" 'NCC-' + 1701 "NCC-" ++ toString 1701 > Work with strings 2 > 1 ? 'Lee' : 'Chan' if 3 > 2 then "Lee" else "Chan" var x = 42; let x = 42 return 42 Everything is an expression, no need for return > Control Flow
  • 9.
    Core > 9 /2 4.5 : Float > 9 // 2 4 : Int > > Values > isNegative n = n < 0 <function> > isNegative 4 False > isNegative -7 True > isNegative (-3 * -4) False > Functions
  • 10.
    Data Structures > Lists >names = [ "Alice", "Bob", "Chuck" ] ["Alice","Bob","Chuck"] > List.isEmpty names False > List.length names 3 > List.reverse names ["Chuck","Bob","Alice"] > numbers = [1,4,3,2] [1,4,3,2] > List.sort numbers [1,2,3,4] > double n = n * 2 <function> > List.map double numbers [2,8,6,4]
  • 11.
    List != Object
 elm!= OOP List = Module
  • 12.
    List module module List (isEmpty, length, reverse, member , head, tail, filter, take, drop , repeat, (::), append, concat, intersperse , partition, unzip , map, map2, map3, map4, map5 , filterMap, concatMap, indexedMap , …………… {-| Determine if a list is empty. isEmpty [] == True -} isEmpty : List a -> Bool isEmpty xs = case xs of [] -> True _ -> False {-| Keep only elements that satisfy the predicate. filter isEven [1..6] == [2,4,6] -} filter : (a -> Bool) -> List a -> List a filter pred xs = let conditionalCons x xs' = if pred x then x :: xs' else xs' in foldr conditionalCons [] xs
  • 13.
    Data Structures > Tuples >import String > goodName name = | if String.length name <= 20 then | (True, "name accepted!") | else | (False, "name was too long; please limit it to 20 characters") > goodName "Tom" (True, "name accepted!")
  • 14.
    Data Structures > Records >point = { x = 3, y = 4 } { x = 3, y = 4 } > point.x 3 > bill = { name = "Gates", age = 57 } { age = 57, name = "Gates" } > bill.name “Gates" > .name bill "Gates" > List.map .name [bill,bill,bill] ["Gates","Gates","Gates"]
  • 15.
    Data Structures > Records >under70 {age} = age < 70 <function> > under70 bill True > under70 { species = "Triceratops", age = 68000000 } False > { bill | name = "Nye" } { age = 57, name = "Nye" } > { bill | age = 22 } { age = 22, name = "Gates" }
  • 16.
    JS Object vsElm Record - You cannot ask for a field that does not exist. - No field will ever be undefined or null. - You cannot create recursive records with a this or self keyword.
  • 17.
    Contracts import String fortyTwo :Int fortyTwo = 42 drinks : List String drinks = [ "Hennessy", "JimBeam", "Jack Daniels" ] book : { title: String, author: String, pages: Int } book = { title = "Robert", author = "Martin", pages = 434 } longestName : List String -> Int longestName drinks = List.maximum (List.map String.length drinks) isLong : { record | pages : Int } -> Bool isLong book = book.pages > 400
  • 18.
    All of thesetypes can be inferred, so you can leave off the type annotations and Elm can still check that data is flowing around in a way that works. This means you can just not write these contracts and still get all the benefits!
  • 19.
    Enumerations type Visibility =All | Active | Completed toString : Visibility -> String toString visibility = case visibility of All -> "All" Active -> "Active" Completed -> "Completed" -- toString All == "All" -- toString Active == "Active" -- toString Completed == "Completed"
  • 20.
    State Machines type User= Anonymous | LoggedIn String userPhoto : User -> String userPhoto user = case user of Anonymous -> "anon.png" LoggedIn name -> "users/" ++ name ++ "/photo.png"
  • 21.
  • 22.
    MUV -- MODEL type aliasModel = { ... } -- UPDATE type Action = Reset | ... update : Action -> Model -> Model update action model = case action of Reset -> ... ... -- VIEW view : Model -> Html view = ...
  • 23.
    Example type alias Model= { topic : String , gifUrl : String } init : String -> (Model, Effects Action) init topic = ( Model topic "assets/waiting.gif" , ge update : Action -> Model -> (Model, Effects Action) update action model = case action of RequestMore -> (model, getRandomGif model.topic) NewGif maybeUrl -> ( Model model.topic (Maybe.withDefault model.gifUrl maybeUrl) , Effects.none ) Model Update
  • 24.
    Example getRandomGif : String-> Effects Action getRandomGif topic = Http.get decodeUrl (randomUrl topic) |> Task.toMaybe |> Task.map NewGif |> Effects.task randomUrl : String -> String randomUrl topic = Http.url "http://api.giphy.com/v1/gifs/random" [ "api_key" => "dc6zaTOxFJmzC" , "tag" => topic ] decodeUrl : Json.Decoder String decodeUrl = Json.at ["data", "image_url"] Json.string Effects
  • 25.
  • 26.
  • 27.
  • 28.
    Signals module Signal ( Signal ,merge, mergeMany , map, map2, map3, map4, map5 , constant , dropRepeats, filter, filterMap, sampleOn , foldp , Mailbox, Address, Message , mailbox, send, message, forwardTo ) where
  • 29.
    Tasks elm-http — talkto servers elm-history — navigate browser history elm-storage — save info in the users browser
  • 30.
    elm-storage import Native.Storage getItemAsJson :String -> Task String Value getItemAsJson = Native.Storage.getItemAsJson -- Do better error detection getItem : String -> Decoder value -> Task String value getItem key decoder = let decode value = case decodeValue decoder value of Ok v -> succeed v Err err -> fail "Failed" in getItemAsJson key `andThen` decode
  • 31.
    Native.Storage aka Storage.js /*! localForage-- Offline Storage, Improved Version 1.2.2 https://mozilla.github.io/localForage (c) 2013-2015 Mozilla, Apache License 2.0 */ (function() { var define, requireModule, require, requirejs; (function() { var registry = {}, seen = {}; define = function(name, deps, callback) { registry[name] = { deps: deps, callback: callback }; }; requirejs = require = requireModule = function(name) { requirejs._eak_seen = registry; if (seen[name]) { return seen[name]; } seen[name] = {};
  • 32.
    Mailbox type alias Mailboxa = { signal : Signal a , address : Address a } send : Address a -> a -> Task x () import Graphics.Element exposing (Element, show) import Task exposing (Task, andThen) import TaskTutorial exposing (getCurrentTime, print) main : Signal Element main = Signal.map show contentMailbox.signal contentMailbox : Signal.Mailbox String contentMailbox = Signal.mailbox "" port updateContent : Task x () port updateContent = Signal.send contentMailbox.address "hello!" Init with empty value send a message signature
  • 33.
    Ports - Communicationbetween Elm and pure JS
  • 34.
    Ports | JS> Elm port addUser : Signal (String, UserRecord) > Elm > JS myapp.ports.addUser.send([ "Tom", { age: 32, job: "lumberjack" } ]); myapp.ports.addUser.send([ "Sue", { age: 37, job: "accountant" } ]);
  • 35.
    Ports | Elm> JS port requestUser : Signal String port requestUser = signalOfUsersWeWantMoreInfoOn > Elm > JS myapp.ports.requestUser.subscribe(databaseLookup); function databaseLookup(user) { var userInfo = database.lookup(user); myapp.ports.addUser.send(user, userInfo); }
  • 37.
    HTML generation main :Signal Element main = Signal.map2 renderStamps Window.dimensions clickLocations clickLocations : Signal (List (Int,Int)) clickLocations = Signal.foldp (::) [] (Signal.sampleOn Mouse.clicks Mouse.position) renderStamps : (Int,Int) -> List (Int,Int) -> Element renderStamps (w,h) locs = let pentagon (x,y) = ngon 5 20 |> filled (hsla (toFloat x) 0.9 0.6 0.7) |> move (toFloat x - toFloat w / 2, toFloat h / 2 - toFloat y) |> rotate (toFloat x) in layers [ collage w h (List.map pentagon locs) , show "Click to stamp a pentagon." ] elm-make Stamper.elm --output=Main.html
  • 38.
    Nice architecture Good documentation Sourcecode with comments Great developer toolset Own packages / manager Overview from JS perspective
  • 40.
    Complex syntax Learning curve Teamintegration cost? Overview from JS perspective
  • 41.