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

  • 231
Loading comments...

More from Jack Franklin