elm: give it a try
TRANSCRIPT
elm: give it a tryEugene 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 Right [EDecl InfixDecl{}] -> return () -- can ignore infix
It was Haskell
Real Elmresults : 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-compilercompiler, yep
elm-makebuild tools
compile to JS or HTML build dependencies
elm-reactor dev tools
hot swap time travel debugging
elm-replets you interact with values and functions directly.
elm-packagepackage 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 / 24.5 : Float> 9 // 24 : Int>
> Values
> isNegative n = n < 0<function>
> isNegative 4False
> isNegative -7True
> isNegative (-3 * -4)False
> Functions
Data Structures
> Lists
> names = [ "Alice", "Bob", "Chuck" ]["Alice","Bob","Chuck"]
> List.isEmpty namesFalse
> List.length names3
> 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 != Objectelm != OOP
List = Module
List modulemodule 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.
Contractsimport 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 Machinestype 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
ExamplegetRandomGif : 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
Signalsmodule 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 serverselm-history — navigate browser historyelm-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
[email protected] @2j2e