Introduction to Elm
Slides
I'll show you some content on some slides where we'll chat things through.
Exercise
I'll then give you some exercises to do on your machine and answer any questions.
Please ask questions!
Huge thanks and credit to Richard Feldman and his Elm workshop
https://github.com/jackfranklin
/introduction-to-elm-workshop
Setup instructions:
Most JS developers today are writing JavaScript that gets compiled into "older" JavaScript that can run in more Browsers.
But if our code is getting compiled anyway...could we use something other than JS that gives us benefits?
const add = (x, y) => x + y
add x y = x + y
add x y =
x + y
Elm
JS
No parens or commas with function arguments
function name comes first, no need for const/let/var
no explicit return keyword, ever
const pluralize = (singular, plural, count) => {
if (count === 1) return singular
return plural
}
pluralize("apple", "apples", 5) => "apples"
pluralize singular plural number =
if number == 1 then
singular
else
plural
pluralize "apple" "apples" 5
Elm
JS
Exercise 1
npm run exercise exercise1
yarn run exercise exercise1
http://localhost:8000
Let's dig through the code we have...
module Main exposing (..)
import Html exposing (..)
pluralize singular plural number =
plural
main =
text (pluralize "apple" "apples" 5)
module Main exposing (..)
import Html exposing (..)
pluralize singular plural number =
plural
main =
text (pluralize "apple" "apples" 5)
Each Elm file is a module. The name matches the file name.
module Main exposing (..)
import Html exposing (..)
pluralize singular plural number =
plural
main =
text (pluralize "apple" "apples" 5)
here we're importing the HTML module (we'll look more at this later...)
module Main exposing (..)
import Html exposing (..)
pluralize singular plural number =
plural
main =
text (pluralize "apple" "apples" 5)
Elm looks for a function called main, and will run it if it finds it
module Main exposing (..)
import Html exposing (..)
pluralize singular plural number =
plural
main =
text (pluralize "apple" "apples" 5)
`text` is an Html function that outputs Html to the page
module Main exposing (..)
import Html exposing (..)
pluralize singular plural number =
plural
main =
text (pluralize "apple" "apples" 5)
we pass text the result of calling pluralize
Exercise 1
pluralize singular plural number =
plural
-- THIS IS A COMMENT
can you fill this function body in?
HTML
import Html exposing (..)
http://package.elm-lang.org/packages/elm-lang/html/2.0.0/Html
div [ class "content" ] [ text (pluralize "apple" "apples" 5) ]
import Html.Attributes exposing (..)
div [ class "content" ] [ text (pluralize "apple" "apples" 5) ]
div
[ class "content" ]
[ text "Hello world" ]
first argument is a list of attributes
second is a list of children elements
Exercise 2
main =
div [ class "content" ]
[ text (pluralize "apple" "apples" 5)
]
Types and the compiler
pluralize singular plural number =
if number == 1 then
singular
else
plural
pluralize "apple" "apples" "woops"
this is wrong - should be a number
The compiler has our backs!
pluralize : String -> String -> Int -> String
pluralize singular plural number =
the compiler is great at inferring types
but if we know them, we can add them
the right hand type is the return type
Exercise 3
fix the type error that's preventing the app from compiling
and add the type annotation to pluralize
More data types
jack = ("jack", 25)
Tuple.first jack == "jack"
Tuple.second jack == 25
otherJack = ("jack", 25, "london")
-- you cannot use Tuple.first or Tuple.second
-- on tuples of length != 2
Tuples
can contain data of different types, but you can't give any of the data names
stick to 2/3 items at most.
jack =
{ name = "jack"
, age = 25
, city = "london"
}
jack.name == "jack"
jack.age == 25
jack.city == "london"
Records
you cannot add or remove items
types within them can be mixed
fruits =
{ apple = ( "apple", "apples" )
, banana = ( "banana", "bananas" )
, grape = ( "grape", "grapes" )
}
main =
div [ class "content" ]
[ h1 [] [ text "Lots of fruits" ]
, text (pluralize (Tuple.first fruits.banana) (Tuple.second fruits.banana) 5)
, hr [] []
, text (pluralize (Tuple.first fruits.apple) (Tuple.second fruits.apple) 5)
, hr [] []
, text (pluralize (Tuple.first fruits.grape) (Tuple.second fruits.grape) 1)
]
text
(pluralize (Tuple.first fruits.banana) (Tuple.second fruits.banana) 5)
text
(pluralize "banana" "bananas" 5)
if you're thinking we could write this more neatly...you'd be correct! We'll get there soon :)
Exercise 4
more type annotations!
fruits :
{ apple : ( String, String )
, banana : ( String, String )
, grape : ( String, String )
}
phew, that code was a bit messy...
pluralize (Tuple.first fruits.banana) (Tuple.second fruits.banana) 5
-- becomes:
pluralize fruits.banana 5
What if pluralize took a fruit?
apple : ( String, String )
pluralize: ( String, String ) -> Int -> String
pluralize fruit number =
if number == 1 then
Tuple.first fruit
else
Tuple.second fruit
what is a fruit?
type alias Fruit = ( String, String )
apple : Fruit
pluralize: Fruit -> Int -> String
pluralize fruit number =
if number == 1 then
Tuple.first fruit
else
Tuple.second fruit
type aliases
Exercise 5
type alias Fruit = ( String, String )
apple : Fruit
pluralize: Fruit -> Int -> String
pluralize fruit number =
if number == 1 then
Tuple.first fruit
else
Tuple.second fruit
Lists
people = ["Jack", "Alice", "Bob"]
jack = ["Jack", 2, 2.22] --NOPE
Lists
this is invalid
Lists must all have the same type.
people = ["Jack", "Alice", "Bob"]
jack = ["Jack", 2, 2.22] --NOPE
Lists
we say this list is of type: List String
type alias Fruit =
( String, String )
fruits : List Fruit
fruits =
[ ( "apple", "apples" ), ( "banana", "bananas" ) ]
what if our fruits were a list?
type alias Fruit =
( String, String, Int )
fruits : List Fruit
fruits =
[ ( "apple", "apples", 2 ), ( "banana", "bananas", 1 ) ]
let's add a count to our fruit
pluralize : Fruit -> String
pluralize fruit =
-- HOW DO WE GET number from the fruit Tuple?
if number == 1 then
Tuple.first fruit
else
Tuple.second fruit
but how do we get the count in pluralize?
pluralize : Fruit -> String
pluralize ( singular, plural, number ) =
if number == 1 then
singular
else
plural
we can destructure tuples in function args
but how do we now loop through our fruits?
renderFruit : Fruit -> Html a
renderFruit fruit =
text (pluralize fruit)
let's define renderFruit
ignore this for now...we'll get there soon!
renderFruit : Fruit -> Html a
renderFruit fruit =
text (pluralize fruit)
text <| pluralize fruit
pluralize fruit |> text
alternative definitions
these are all equivalent
renderFruit : Fruit -> Html a
renderFruit =
pluralize >> text
we could go one step further...
don't worry if you don't understand this - this takes some time and isn't important for now!
We want to take each fruit and run it through renderFruit
List.map (\a -> a * 2) [1, 2, 3]
-- [2, 4, 6]
List.map
http://package.elm-lang.org/packages/elm-lang/core/5.1.1/List
this is how we create anonymous functions in Elm, just like in JS
apply the function to each item in this list
double a = a * 2
List.map (\a -> a * 2) [1, 2, 3]
List.map double [1, 2, 3]
-- [2, 4, 6]
List.map
http://package.elm-lang.org/packages/elm-lang/core/5.1.1/List
main =
div [ class "content" ]
[ h1 [] [ text "Lots of fruits" ]
, div [] (List.map renderFruit fruits)
]
We can map our fruits via renderFruits
Exercise 6
phew!
renderFruit : Fruit -> Html a
renderFruit fruit =
text (pluralize fruit)
Tuples to records
Let's rewrite fruits to be a record, and see how the compiler helps us
type alias Fruit =
{ singular : String
, plural : String
, number : Int
}
The definition of `fruits` does not match its type annotation.
18| fruits : List Fruit
19| fruits =
20|> [ ( "apple", "apples", 2 ), ( "banana", "bananas", 1 ) ]
The type annotation for `fruits` says it is a:
List Fruit
But the definition (shown above) is a:
List ( String, String, number )
fruits : List Fruit
fruits =
[ { singular = "apple", plural = "apples", number = 4 }
, { singular = "banana", plural = "bananas", number = 1 }
]
This tuple is causing problems in this pattern match.
26| pluralize ( singular, plural, number ) =
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The pattern matches things of type:
( a, b, c )
But the values it will actually be trying to match are:
Fruit
Detected errors in 1 module.
pluralize : Fruit -> String
pluralize ( singular, plural, number ) =
if number == 1 then
singular
else
plural
pluralize : Fruit -> String
pluralize { singular, plural, number } =
if number == 1 then
singular
else
plural
you can destructure records too by using curly braces
Exercise 7
get the fruits rendering onto the page again
Lists | Tuples | Records |
---|---|---|
✔ can iterate | ❌ cannot iterate | ❌ cannot iterate |
❌all of same type | ✔ can have mixed types within | ✔ can have mixed types within |
❌individual items can't be named | ❌individual items can't be named | ✔ individual items can be named |
Lists, Tuples and Records
Quick break!
Building apps with Elm
The Elm Architecture
Model
View
Update
Model
View
Update
the data of your application
how we render the app
how to deal with events and user interaction
Elm Runtime
view
Html
model
update
main =
Html.beginnerProgram
{ model = initialModel
, view = view
, update = update
}
Html.beginnerProgram
type alias Model =
{ fruits : List Fruit
}
--- the name of the alias doesn't matter
--- but Model is the convention you'll see everywhere
fruits : List Fruit
fruits =
[ { singular = "apple", plural = "apples", number = 4 }
, { singular = "banana", plural = "bananas", number = 1 }
]
initialModel : Model
initialModel =
{ fruits = fruits }
Define the Model alias
view : Model -> Html a
view model =
div
[ class "content" ]
[ h1 [] [ text "Lots of fruits" ]
, div [] (List.map renderFruit [])
]
Define view
notice that view takes the model as its argument
so it can render data from what's in the model
update : a -> Model -> Model
update msg model =
model
Define update
ignore this for now! we're going to come onto this in the next exercise :)
Exercise 8
fix the view so it shows our fruits properly
and have a play with the code to get used to the model/view/update pattern
update
we're going to leave our fruits behind for now...
Handling user interaction
import Html.Events exposing (onClick)
view : Model -> Html a
a here is a placeholder for "some type that we don't know about"
sometimes we don't always know exactly what type we'll be given
List.singleton "foo" --> ["foo"]
List.singleton 2 --> [2]
singleton : a -> List a
singleton a = [a]
this annotation says we take some `a`, and return a List of type `a`
singleton : a -> List a
singleton a = [a]
List.singleton "foo" --> ["foo"] List String
List.singleton 2 --> [2] List Int
So `a` is a generic type, and we can replace it with a specific type.
Elm Runtime
view
Html String
model
update
User actions produce String
view : Model -> Html a
"the view function takes a model and produces HTML that creates messages of type a"
user clicks button
elm runtime generates a message of type `a`
type alias Model =
{ count : Int
}
initialModel : Model
initialModel =
{ count = 0 }
Counter app
view : Model -> Html String
view model =
div
[ class "content" ]
[ h1 [] [ text "Click to increment" ]
, div []
[ button [ onClick "INCREMENT" ]
[ text "Increment" ]
, text (toString model.count)
]
]
using onClick in view
view : Model -> Html String
view model =
div
[ class "content" ]
[ h1 [] [ text "Click to increment" ]
, div []
[ button [ onClick "INCREMENT" ]
[ text "Increment" ]
, text (toString model.count)
]
]
using onClick in view
onClick "INCREMENT" means we create messages of type String, so our view is producing Html String
update : String -> Model -> Model
update msg model =
if msg == "INCREMENT" then
-- do something here
else
model
dealing with the message in update
notice that the first argument to update is of type String, because it's taking the messages that our Html produces.
model = { count = 0 }
{ model | count = 1 } -- { count = 1 }
{ model | count = model.count + 1 } -- { count = 1 }
updating a record
remember that all data is immutable, so doing this creates a new record, and leaves the old one intact!
update : String -> Model -> Model
update msg model =
if msg == "INCREMENT" then
{ model | count = model.count + 1 }
else
model
update our model when "INCREMENT" occurs
Exercise 9
the decrement button
Strings are bad to use as messages
update : String -> Model -> Model
update msg model =
if msg == "INCREMENT" then
-- code here removed to save space!
view : Model -> Html String
view model =
div
[ class "content" ]
[ h1 [] [ text "Click to increment" ]
, div []
[ button [ onClick "INCREENT" ]
[ text "Increment" ]
, text (toString model.count)
]
]
spot the bug
update : String -> Model -> Model
update msg model =
if msg == "INCREMENT" then
-- code here removed to save space!
view : Model -> Html String
view model =
div
[ class "content" ]
[ h1 [] [ text "Click to increment" ]
, div []
[ button [ onClick "INCREENT" ]
[ text "Increment" ]
, text (toString model.count)
]
]
spot the bug
message typo!
can we handle messages in a way that lets the compiler tell us when we typo?
Union types to the rescue!
in general, if you make a mistake and the compiler doesn't tell you about it, you're not doing it in the "Elm" way
if msg == "INCREMENT" then
...
else if msg == "DECREMENT" then
...
else
...
Multiple messages
case msg of
"INCREMENT" ->
{ model | count = model.count + 1 }
"DECREMENT" ->
{ model | count = model.count - 1 }
_ ->
model
Case statements
type Msg = Increment | Decrement
type Bool
= True
| False
Union types
this is the type
and these are constants
so we say "True is of type Bool"
type Msg = Increment | Decrement
update : Msg -> Model -> Model
Union types
now update can only take messages of type Msg, which are Increment or Decrement. This is enforced by the compiler for us!
(note: you could call this type anything, but Msg is the Elm convention)
view : Model -> Html Msg
view model =
div
[ class "content" ]
[ h1 [] [ text "Click to increment" ]
, div []
[ button [ onClick Increment ]
[ text "Increment" ]
, text (toString model.count)
]
]
Union types
now our view produces Html Msg
And now the compiler has our back...
type Msg
= Increment
| Decrement
update : Msg -> Model -> Model
update msg model =
case msg of
Increment ->
{ model | count = model.count + 1 }
Missing Decrement
This `case` does not have branches for all possibilities.
29|> case msg of
30|> Increment ->
31|> { model | count = model.count + 1 }
You need to account for the following values:
Main.Decrement
compiler win
Cannot find pattern `Derement`
33| Derement ->
^^^^^^^^
Maybe you want one of the following?
Decrement
and no more typos!
Exercise 10
exercise 10 does not compile - can you fix it?
Let's take another break.
Increment by
sometimes our messages have extra data attached to them
we want to add a button to our counter to increment by an amount
type Msg
= Increment
| IncrementBy Int
| Decrement
Union type constructors
IncrementBy is a function
IncrementBy: Int -> Msg
IncrementBy 5 == Msg
button
[ onClick (IncrementBy 5) ]
[ text "Increment by 5" ]
IncrementBy: Int -> Msg
update : Msg -> Model -> Model
update msg model =
case msg of
IncrementBy x ->
{ model | count = model.count + x }
In update...
IncrementBy 5
x == 5
you can destructure on union types
Exercise 11
DecrementBy
User input and parsing of types
Let the user pass in the amount to increment by
import Html.Attributes exposing (class, type_, value)
import Html.Events exposing (onClick, onInput)
Inputs in Elm
if you've used React or others, you might know this as onChange
input
[ type_ "number"
, onInput NewUserIncrementInput
, value model.userInput
]
[]
type Msg
= Increment
| NewUserIncrementInput String
Inputs in Elm
update : Msg -> Model -> Model
update msg model =
case msg of
NewUserIncrementInput newValue ->
{ model | userInput = newValue }
Inputs in Elm
update : Msg -> Model -> Model
update msg model =
case msg of
Increment ->
{ model | count = model.count + model.userInput }
using userInput to increment
The right side of (+) is causing a type mismatch.
28|model.count + model.userInput
^^^^^^^^^^^^^^^
(+) is expecting the right side to be a:
Int
But the right side is:
String
Hint: To append strings in Elm, you need to use the (++) operator, not (+).
But no...
Converting a string to an integer
String.toInt : String -> Result String Int
?????
Converting a string to an int could fail
toInt "LOL" -- ????
toInt "3" -- totally fine
And Elm needs you to deal with potential failures
It does this using Result
type Result error value
= Ok value
| Err error
type Result error value
= Ok value
| Err error
String.toInt "123" == Ok 123
String.toInt "-42" == Ok -42
String.toInt "3.1"
== Err "could not convert string '3.1' to an Int"
String.toInt "31a"
== Err "could not convert string '31a' to an Int"
case msg of
Increment ->
case String.toInt model.userInput of
Ok number ->
{ model | count = model.count + number }
Err _ ->
model
we can use case of to check the result
case String.toInt model.userInput of
Ok number ->
...
Err _ ->
...
Exercise 12!
let expressions
let expressions are a good way to temporarily create a "variable"
case String.toInt model.userInput of
Ok number ->
{ model | count = model.count + number }
Err _ ->
model
this could get hard to read
let
userInputAsNumber = String.toInt model.userInput
in
case userInputAsNumber of
Ok number ->
{ model | count = model.count + number }
Err _ ->
model
so we can lift a value into a let
let
userInputAsNumber = String.toInt model.userInput
in
-- userInputAsNumber is only available within the `in`
so we can lift a value into a let
Exercise 13
let
userInputAsNumber = String.toInt model.userInput
in
...
Dealing with errors
case String.toInt model.userInput of
Ok number ->
{ model | count = model.count + number }
Err _ ->
model
In this case we don't actually care if the input is invalid, we just do nothing.
We can make this cleaner with Result.withDefault
withDefault : a -> Result x a -> a
Result.withDefault 0 (String.toInt "123") == 123
Result.withDefault 0 (String.toInt "abc") == 0
So if String.toInt succeeds, use that, else use 0
withDefault : a -> Result x a -> a
Result.withDefault 0 (String.toInt "123") == 123
Result.withDefault 0 (String.toInt "abc") == 0
Exercise 14
withDefault : a -> Result x a -> a
Result.withDefault 0 (String.toInt "123") == 123
Result.withDefault 0 (String.toInt "abc") == 0
Back to fruits!
type alias Fruit =
{ name : String, count : Int }
type alias Model =
{ fruits : List Fruit
, userFruitNameInput : String
, userFruitCountInput : String
}
type Msg
= StoreNewFruit
| UserFruitNameInput String
| UserFruitCountInput String
initialModel : Model
initialModel =
{ fruits = [ { name = "Apple", count = 5 }, { name = "Banana", count = 4 } ]
, userFruitNameInput = ""
, userFruitCountInput = "0"
}
update : Msg -> Model -> Model
update msg model =
case msg of
StoreNewFruit ->
-- EXERCISE: can you make this add a fruit to the fruits list?
-- you will need to parse the userFruitCountInput to an integer
-- and then create a new fruit to add to model.fruits
model
Exercise 15
-- adding to a list
"bar" :: ["foo"] == ["bar", "foo"]
List.append ["bar"] ["foo"] == ["bar", "foo"]
["bar"] ++ ["foo"] == ["bar", "foo"]
Sorting our fruits
type FruitSorting = CountAsc | CountDesc
type Msg
= StoreNewFruit
| SortFruit FruitSorting
| UserFruitNameInput String
| UserFruitCountInput String
a new Union Type
, div [ class "sorting" ]
[ button [ onClick (SortFruit CountAsc) ] [ text "Count Asc" ]
, button [ onClick (SortFruit CountDesc) ] [ text "Count Desc" ]
]
two buttons
List.sort [3, 1, 5] == [1, 3, 5]
let fruits = [
{ name = "apple", count = 4 }
, { name = "banana", count = 2 }
]
List.sortBy (\fruit -> fruit.count) fruits
== [ banana, apple ]
sorting lists
List.sortBy (\fruit -> fruit.count) fruits
let fruit = { name = "apple", count = 4 }
.count fruit == 4
List.sortBy .count fruits
a nice shortcut
List.reverse (List.sortBy (\fruit -> fruit.count) model.fruits)
--clearer:
List.sortBy (\fruit -> fruit.count) model.fruits
|> List.reverse
-- even better:
model.fruits
|> List.sortBy .count
|> List.reverse
the pipeline operator
Exercise 16
model.fruits
|> List.sortBy .count
|> List.reverse
Pushing state into the model
1. Sort the items
2. Add a new item
3. New item is not put into the right place
we need to store our sort in the model, so it persists at all times and handles new items correctly.
type alias Model =
{ fruits : List Fruit
, userFruitNameInput : String
, userFruitCountInput : String
, fruitSorting : FruitSorting
}
type FruitSorting
= CountAsc
| CountDesc
initialModel : Model
initialModel =
{ fruits = [ { name = "Apple", count = 5 }, { name = "Banana", count = 4 } ]
, userFruitNameInput = ""
, userFruitCountInput = "0"
, fruitSorting = CountAsc
}
update our model
case msg of
SortFruit sortType ->
{ model | fruitSorting = sortType }
and our update
renderFruits : List Fruit -> FruitSorting -> Html Msg
renderFruits fruits fruitSorting =
-- EXERCISE: can you take fruitSorting into account here
-- and order the fruits in the proper order based on the value
-- of fruitSorting
ul [] (List.map renderFruit fruits)
Exercise 17
update renderFruits to take into account our sort state
Let's take a break!
Async work
In Elm, all functions are pure.
This means no side effects, ever.
const fetchStuff = () => fetch('/url')
side effect: HTTP request
when we need to do async work in Elm, we provide a command to the runtime
we say to Elm: "hey, do this work for me and let me know when you're done"
Elm Runtime
view
Html Msg
model
update
Msg
Cmd Msg
"hey Elm, do some work
for me"
Html.program
Html.beginnerProgram
update: Msg -> Model -> Model
Html.program
update: Msg -> Model -> (Model, Cmd Msg)
new model (just like everything we've done so far today)
work for the Elm runtime to do for us in the background
type alias Model =
{ latestNumber : Int
}
type Msg
= NewRandomNumber Int
| RollDice
initialModel : Model
initialModel =
{ latestNumber = 0
}
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
RollDice ->
( model, Cmd.none )
NewRandomNumber x ->
( model, Cmd.none )
Random.generate
NewRandomNumber
(Random.int 1 6)
type: Cmd Msg
Get a random number
the Msg we want it to send us back
what to generate
Random.generate gives us a command we can give to Elm
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
RollDice ->
( model, Random.generate NewRandomNumber (Random.int 1 6) )
when the user hits the button, roll the dice
NewRandomNumber x ->
( { model | latestNumber = x }, Cmd.none )
store the number once we have it
Exercise 18
get the dice rolling :)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
RollDice ->
( model, Random.generate NewRandomNumber (Random.int 1 6) )
NewRandomNumber x ->
( { model | latestNumber = x }, Cmd.none )
Modelling the latest number when we don't have one
type alias Model =
{ latestNumber : Int
}
What we did
latestNumber = -1 ???
But this leads to odd states
In Elm we don't have null, nil, undefined
You have to model all data that might be missing
type Maybe a
= Just a
| Nothing
Maybe
type alias Model =
{ latestNumber : Maybe Int
}
Using Maybe
we might have an Integer
but we might have Nothing
type alias Model =
{ latestNumber : Maybe Int
}
case model.latestNumber of
Just x ->
-- we have a value, x
Nothing ->
-- we have no value yet
Using Maybe
x = 2 -- type: Int
y = Just 3 -- type: Maybe Int
z = Nothing -- type: Maybe Int
Putting a value into maybe
Exercise 19
x = 2 -- type: Int
y = Just 3 -- type: Maybe Int
z = Nothing -- type: Maybe Int
JSON decoding
fetch('/api')
.then(response => response.json())
.then(data => ...)
In Elm all data is strictly typed, so we need to decode JSON into Elm data.
This takes some time to get used to!
Let's decode this JSON into an Elm record
JSON: { "name": "Jack" }
-- and we want:
ELM: { name = "Jack" }
In Elm we create decoders that combine to decode what we need
import Json.Decode as JD
nameDecoder =
JD.field "name" JD.string
creates a decoder that can parse the "name" field from a JSON object, as a string
import Json.Decode as JD
nameDecoder =
JD.field "name" JD.string
JD.decodeString nameDecoder """{"name": "jack"}"""
-- (Ok "jack")
we pass a decoder to decodeString to run it against some JSON
import Json.Decode as JD
nameDecoder =
JD.field "name" JD.string
personDecoder =
JD.map
(\name -> { name = name })
nameDecoder
JD.decodeString personDecoder """{"name": "jack"}"""
-- (Ok { name = "jack" })
We use JD.map to parse JSON into records
import Json.Decode as JD
nameDecoder = JD.field "name" JD.string
ageDecoder = JD.field "age" JD.int
personDecoder =
JD.map2
(\name, age -> { name = name, age = age })
nameDecoder
ageDecoder
JD.decodeString
personDecoder
"""{"name": "jack", "age": 25}"""
-- (Ok { name = "jack", age = 25 })
what if we have multiple fields?
these decode the individual fields
that get passed to the function that's the first argument
type alias Person = {name: String, age: Int}
Person: String -> Int -> Person
Person "Jack" 25 == { name = "Jack", age = 25 }
import Json.Decode as JD
type alias Person = {name: String, age: Int}
nameDecoder = JD.field "name" JD.string
ageDecoder = JD.field "age" JD.int
personDecoder =
JD.map2
Person
nameDecoder
ageDecoder
JD.decodeString
personDecoder
"""{"name": "jack", "age": 25}"""
-- (Ok { name = "jack", age = 25 })
we can take advantage of type constructors
Exercise 20
type alias Model =
{ person : Maybe (Result String Person)
}
personDecoder : JD.Decoder a
personDecoder =
JD.fail "You have not built the decoder yet!"
nameDecoder = JD.field ...
ageDecoder = JD.field ...
personDecoder =
JD.map2 ...
APIs
We now have all the pieces in place to make API requests
Do async work via Elm Commands and parse JSON via decoders.
http://github-proxy-api.herokuapp.com/users/jackfranklin
{
login: "jackfranklin",
id: 193238,
name: "Jack Franklin",
company: "@thread ",
blog: "http://www.jackfranklin.co.uk",
location: "London",
bio: "JavaScript, React, ES2015+ and Elm.",
public_repos: 257,
public_gists: 71
}
elm package install elm-lang/http
Elm has its own HTTP library but it's not built into the core, so you need to install it.
(I've already installed it for us into our project!)
type alias Person =
{ name : String
, publicRepoCount : Int
}
type alias Model =
{ person : Maybe Person
}
type Msg
= FetchPerson
| GotPerson (Result Http.Error Person)
Structuring our app
update msg model =
case msg of
FetchPerson ->
( model, fetchPerson )
fetchPerson = Cmd.none
Triggering a fetch
fetchPerson =
let
url = "..."
request =
Http.get url personDecoder
in
Http.send GotPerson request
Http.get and Http.send
fetchPerson =
let
url = "..."
request =
Http.get url personDecoder
in
Http.send GotPerson request
Http.get and Http.send
the decoder to deal with the JSON response
the Msg that will be generated once the request has been made
GotPerson (Result Http.Error Person)
Exercise 21!
fetchPerson =
let
url = "..."
request =
Http.get ...
in
Http.send PutSomethingHere request
Talking to JavaScript
It's not always possible to stay just in Elm land.
Elm provides us with ports that let us send data to and from JavaScript.
Let's imagine we can only fetch data from GitHub in JavaScript
Port
A way to send data from Elm to JavaScript.
Subscription
A way for the Elm runtime to listen to data that JavaScript sends back to it.
Elm Runtime
view
Html Msg
model
update
Msg
Cmd Msg
"hey Elm, do some work
for me"
subscriptions
port githubSearch : String -> Cmd msg
-- the Cmd that we will send from Elm to JS: githubSearch "jackfranklin"
port githubResults : (JD.Value -> msg) -> Sub msg
-- the subscription that we'll get back
-- JD.Value here is a type meaning "Some JS value that we need to decode"
port githubResults : (JD.Value -> msg) -> Sub msg
-- the subscription that we'll get back
-- JD.Value here is a type meaning "Some JS value that we need to decode"
main : Program Never Model Msg
main =
Html.program
{ init = init
, view = view
, update = update
, subscriptions = \_ -> githubResults handleGithubResponse
}
app.ports.githubSearch.subscribe(value => ...)
app.ports.githubResponse.send(data)
In JS land
Exercise 22!
app.ports.githubSearch.subscribe(value => ...)
app.ports.githubResponse.send(data)
handleGithubResponse : JD.Value -> Msg
handleGithubResponse valueFromGithubJS =
...
That's a wrap!
Questions?
@Jack_Franklin
jack@jackfranklin.net
javascriptplayground.com - Elm course coming soon!
Thank you!
I will leave all the GitHub repository and slides available to you via the online links so you can refer to them :)
Bonus content!
Better decoding
https://github.com/NoRedInk/
elm-decode-pipeline
elm package install NoRedInk/elm-decode-pipeline
personDecoder : JD.Decoder Person
personDecoder =
JD.map2 Person
(JD.field "name" JD.string)
(JD.field "age" JD.int)
What we had
what if we had more fields?
We can have up to 8...
Now, with pipeline...
import Json.Decode.Pipeline as JP
personDecoder : JD.Decoder Person
personDecoder =
JP.decode Person
|> JP.required "name" JD.string
|> JP.required "age" JD.int
pipeline decoding
import Json.Decode.Pipeline as JP
personDecoder : JD.Decoder Person
personDecoder =
JP.decode Person
|> JP.required "name" JD.string
|> JP.required "age" JD.int
comparsion
personDecoder : JD.Decoder Person
personDecoder =
JD.map2 Person
(JD.field "name" JD.string)
(JD.field "age" JD.int)
Old code
with pipeline
Introduction to Elm
By Jack Franklin
Introduction to Elm
- 1,152