Highway

@JoGrenat #elmlang

to

Elm

Highway

to

Elm

Jordane Grenat

@JoGrenat

Front-end is dominated by

JavaScript is backward compatible

Biggest advantage

Biggest disadvantage

You can't deprecate things !!! 😱

null > 0  // false

null == 0 // false

null >= 0 // ... true

JS syntax size

Time

ES6

A LOT to learn!

  • The language is complex
  • Many ways to do the same thing
  • Framework choice / learning
  • Tooling

I call it my billion-dollar mistake. It was the invention of the null reference in 1965. [...] This has led to innumerable errors, vulnerabilities, and system crashes [...]

– Tony Hoare –

Hard to optimize

  • Dead code elimination / Tree shaking
  • Assets size
  • Complex build

Low confidence level

  • Minor IDE assistance
  • Refactoring is painful
  • Slow feedback cycle: runtime
  • Dependencies are risky

By Chris Williams

Elm is a typed functional language to create fast and reliable web applications

Websites and webapps

Domain specific

Statically typed

  • Many bugs don't compile
  • IDE can help you
  • Refactoring is compiler-driven
  • Feedback cycle is really fast

No Runtime Exceptions

17K+ LoC

200K+ LoC

Functional programming is a way of writing programs by using pure functions and avoiding mutable data and side-effects

A pure function's output only depends on its inputs 

and has no side effect

function getRandomValue() {
    return Math.random();
}
let numbersList = [];

function addNumberToList(number) {
    numbersList.push(number);
    return number;
}
getRandomValue(); // 0.9613065251542985
getRandomValue(); // 0.5225738715986086

Declarative, not imperative

const myNumbers = [1, 2, 3, 4, 5];
let squaredNumbers = [];

for(number of myNumbers) {
    squaredNumbers.push(number * number);
}
const myNumbers = [1, 2, 3, 4, 5];
const squaredNumbers = myNumbers
    .map(number => number * number);

Immutability

numbers = [1, 2, 3]

-- ...

numbers.push 4

Honesty

function sendEmail(user) {
    



}

No unwanted side-effect

function sendEmail(user) {
    if (user.email === null) {
        throw new Error('No email provided for the user');
    }    

}
function sendEmail(user) {
    if (user.email === null) {
        throw new Error('No email provided for the user');
    }    
    launchNuclearMissile();
}

Side-effect = command, explicitely declared

Fast

  • Everything is pure => optimizations
  • Small assets

"Elm has a very strong emphasis on simplicity, ease-of-use, and quality tooling."

Simplicity, ease of use

  • Focused on one purpose
  • Syntax removal
  • Well-thought API
  • Naming
  • Explicitness

Quality tooling

  • Compiler
  • Project starter
  • Package manager
  • Dev environment
  • REPL
  • Tests runner
  • Doc tool

Enforced documentation

Enforced versioning

1.2.3

Nothing changed for the outside

Something was added

Something has changed

Great community

See also Building Trust: What Has Worked by Richard Feldman 

and What is success? by Evan Czaplicki

Great community

2012

Evan Czaplicki (@czaplic)

0.1 -> 0.6

2013

0.7 -> 0.10.1

2014

0.11 -> 0.14

New package
manager

Better communication
with JavaScript

2015

0.14.1 -> 0.16

2016

0.17, 0.18

Evan Czaplicki hired at NoRedInk

2018

Current version: 0.19

Wait, V0.19!?

Syntax

-- a single line comment

{- a 
   multiline 
   comment
-}

Comments

myString : String
myString = "Hello"


add : Int -> Int -> Int
add a b = a + b

Type annotations

myUser : { login : String, password : String }
myUser = { login = "Jordane", password = "passw0rd" }


updatedUser = 
    { myUser | password = "S3cur3d" }

-- { login = "Jordane", password = "S3cur3d" }

Record update

let





in
  twentyFour + sixteen
3 * 8 + 4 * 4




let
  twentyFour =
    3 * 8

  sixteen =
    4 * 4
in
  twentyFour + sixteen

let ... in

myNumbers =
    [2, 3, 4]

allNumbers =
    1 :: myNumbers

-- [1, 2, 3, 4]

Prepend element to list

case myNumber of
    0 ->
        "Zero"
    1 -> 
        "One"
    anythingElse -> 
        "Nor 0 nor 1"

case ... of

case myNumber of
    0 ->
        "Zero"
    1 -> 
        "One"
    _ -> 
        "Nor 0 nor 1"
case myStringMaybe of
    Just myString ->
        "myStringMaybe contains: " ++ myString
    Nothing ->
        "myStringMaybe contains nothing"

case ... of

myStringMaybe = Just "Hello"
"Hello"
case myList of
    [] ->
        "The list is empty"
    firstElement :: tail ->
        "First element: " ++ firstElement

case ... of

myList = ["first", "second", "third"]
"first"
["second", "third"]

Pipeline operator

value : String
value =
    String.toUpper (String.trim (String.repeat 2" hello"))

-- "HELLO HELLO"
value : String
value =
    " hello"
    |> String.repeat 2
    |> String.trim
    |> String.toUpper 

" hello"

" hello hello"

"hello hello"

Modules

module Map exposing (navigate)

import Random
import Html exposing (div, h1)

What we'll build

Ellie

Elm online editor

Editors

VS Code

IntelliJ

SublimeText

Emacs

Brackets

Vim

List.map

myList : List Int
myList = [1, 2, 3, 4, 5]

squared : List Int
squared = 
    List.map (\x -> x * x) myList

-- [1, 4, 9, 16, 25]

List.map

myList : List String
myList = ["a", "b"]

myHtmlList : Html msg
myHtmlList =
    
    List.map toLiElement myList
        

toLiElement : String -> Html Msg
toLiElement label =
    li [] [ text label ] 
    List.map toLiElement myList
        |> ul []

toLiElement : String -> Html Msg
toLiElement label =
    li [] [ text label ] 

List (Html Msg)

List.filter

numbers : List Int
numbers = [1, 2, 3, 4, 5]

isEven : Int -> Bool
isEven number =
    number % 2 == 0
numbers : List Int
numbers = [1, 2, 3, 4, 5]

isEven : Int -> Bool
isEven number =
    number % 2 == 0

evenNumbers : List Int
evenNumbers = List.filter isEven numbers
    
-- [2, 4]

The
Elm
Architecture

Model

view()

update()

Html Msg

Elm Runtime

Msg

Html

Event

(new) Model

Program: Sandbox

main : Program () Model Msg
main = 
    Browser.sandbox
        { init = initialModel, view = view, update = update }
main : Program () Model Msg
main = 
    Browser.sandbox
        { init = initialModel, view = view, update = update }

type alias Model = { counterValue : Int }
main : Program () Model Msg
main = 
    Browser.sandbox
        { init = initialModel, view = view, update = update }

type alias Model = { counterValue : Int }

view : Model -> Html Msg
view model = 
    -- Display view according to model
main : Program () Model Msg
main = 
    Browser.sandbox
        { init = initialModel, view = view, update = update }

type alias Model = { counterValue : Int }

view : Model -> Html Msg
view model = 
    -- Display view according to model

type Msg = 
    Increment | Decrement
main : Program () Model Msg
main = 
    Browser.sandbox
        { init = initialModel, view = view, update = update }

type alias Model = { counterValue : Int }

view : Model -> Html Msg
view model = 
    -- Display view according to model

type Msg = 
    Increment | Decrement

update : Msg -> Model -> Model
update msg model = 
    -- Return new model according to message and former model

Program: Sandbox

update : Msg -> Model -> Model
update msg model = 
    case msg of 
        Increment ->
            { model | counterValue = model.counterValue + 1 }

        Decrement ->
            { model | counterValue = model.counterValue - 1 }

Shuffle answers

shuffleAnswers answers
-- ["Rouge", "Noir", "Blanc", "Vert"]

shuffleAnswers answers
-- ["Noir", "Blanc", "Vert", "Rouge"]

Ask the Elm Runtime to do it!

Effects

Model

view()

update()

Html Msg

Elm Runtime

Msg

Model

Model

Msg

Cmd Msg

Program: Element

main : Program Value Model Msg
main = 
    Browser.element 
        { init = init
        , view = view
        , update = update
        , subscriptions = subscriptions
        }
main : Program Value Model Msg
main = 
    Browser.element 
        { init = init
        , view = view
        , update = update
        , subscriptions = subscriptions
        }

init : Value -> (Model, Cmd Msg)
main : Program Value Model Msg
main = 
    Browser.element 
        { init = init
        , view = view
        , update = update
        , subscriptions = subscriptions
        }

init : Value -> (Model, Cmd Msg)

update : Msg -> Model -> (Model, Cmd Msg)
main : Program Value Model Msg
main = 
    Browser.element 
        { init = init
        , view = view
        , update = update
        , subscriptions = subscriptions
        }

init : Value -> (Model, Cmd Msg)

update : Msg -> Model -> (Model, Cmd Msg)

subscriptions : Model -> Sub Msg

Random

  • Take this question
  • Get all the answers
  • Pick them in a random order
  • Put them in the question

Runtime

Generator Question

Cmd Msg

Random

type Msg = 
    GenerateInt
    | IntGenerated Int


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of 
        GenerateInt -> 
            ( model, Random.generate IntGenerated Random.int )

        IntGenerated int -> 
            ( { model | randomInt = int }, Cmd.none )

Generator Int

Cmd Msg

Get question

question

No Runtime Exceptions

bug

bug

bug

bug

bug

bug

bug

bug

bug

External World

Border
Control

Http.get

type Msg = 
    StringReceived (Result Http.Error String)
type Msg = 
    StringReceived (Result Http.Error String)

myRequest : Cmd Msg
myRequest =
    Http.get 
        { url = "http://my.url/page.txt"
        , expect = expectString StringReceived
        }

Result

type Result
    = Err Http.Error
    | Ok String
Err (Failure "No internet connection")
Ok "What color is the white horse of..."
type alias Question = 
    { label: String
    , correctAnswer : String
    , otherAnswers : List String
    }
{
  "question": "What color is the white horse of Henri IV?",
  "correctAnswer": "White",
  "otherAnswers": ["Blue", "Red", "Yellow"]
}
{
  "question": "What color is the white horse of Henri IV?",
  "correctAnswer": "White"

}
{
  "question": "What color is the white horse of Henri IV?",
  "corrctAnswer": "White",
  "otherAnswers": "blue, red, yellow"
}

"Okay, what you should see is an object"

"It should contain a key label which contains a string"

"And also a key correctAnswer which contains a string"

"And also a key incorrectAnswers which contains a list of strings"

"Yep, that's it, here is your Question"

"No, that doesn't match, I'm not seing any field with key incorrectAnswers"

Result

Ok result

Err reason

Decoder

import Json.Decode as Decode


result : Result Decode.Error Int
result = 
    Decode.decodeString Decode.int "12"


-- Err (Failure "This is not valid JSON! Unexpected token h in JSON at position 0")
import Json.Decode as Decode


result : Result Decode.Error Int
result = 
    Decode.decodeString Decode.int "12"
import Json.Decode as Decode


result : Result Decode.Error Int
result = 
    Decode.decodeString Decode.int "12"

-- Ok 12


result2 : Result Decode.Error Int
result2 = 
    Decode.decodeString Decode.int "hello"

List decoder

import Json.Decode as Decode

json : String
json = """["a", "b", "c"]"""


result : Result Decode.Error (List String)
result = 
    Decode.decodeString (Decode.list Decode.string) json
-- Ok ["a", "b", "c"]

Object decoder

import Json.Decode as Decode

json = """{"name": "Jordane", "age": 28, "language": "Elm"}"""

result : Result Decode.Error String
result = 
    Decode.decodeString (Decode.field "name" Decode.string) json
-- Ok "Jordane"
import Json.Decode as Decode

type User = {username : String, age : String}

json = """{"name": "Jordane", "age": 28, "language": "Elm"}"""

result : Result Decode.Error User
result = 
    Decode.decodeString userDecoder json

    
import Json.Decode as Decode

type User = {username : String, age : String}

json = """{"name": "Jordane", "age": 28, "language": "Elm"}"""

result : Result Decode.Error User
result = 
    Decode.decodeString userDecoder json

userDecoder : Decode.Decoder User
userDecoder = 
    Decode.map2 (\name age -> User name age)
        (Decode.field "name" Decode.string)
        (Decode.field "age" Decode.int)
import Json.Decode as Decode

type User = {username : String, age : String}

json = """{"name": "Jordane", "age": 28, "language": "Elm"}"""

result : Result Decode.Error User
result = 
    Decode.decodeString userDecoder json

userDecoder : Decode.Decoder User
userDecoder = 
    Decode.map2 User
        (Decode.field "name" Decode.string)
        (Decode.field "age" Decode.int)

Http.get & JSON

type Msg = 
    QuestionReceived (Result Http.Error Question)
type Msg = 
    QuestionReceived (Result Http.Error Question)

getQuestion : Cmd Msg
getQuestion =
    Http.get 
        { url = "http://my.url/question.json"
        , expect = expectJson QuestionReceived questionDecoder
        }

Let's talk about

modelization

type alias Model =
    { currentQuestion : Int
    , questions : List Question 
    }
myModel = 
    { currentQuestion = 0
    , questions = []
    }
myModel = 
    { currentQuestion = 9
    , questions = [question1]
    }
type alias Model =
    { currentQuestion : Question
    , questions : List Question 
    }
myModel = 
    { currentQuestion = question2
    , questions = [question1, question3]
    }
type alias Model =
    { currentQuestion : Question
    , remainingQuestions : List Question 
    , answeredQuestions : List Question
    }
myModel = 
    { currentQuestion = question2
    , remainingQuestions = [question3]
    , answeredQuestions = [question1]
    }
type alias Model =
    { currentQuestion : Question
    , remainingQuestions : List Question 
    , answeredQuestions : List Question
    , answers : List String
    }
myModel = 
    { currentQuestion = question2
    , remainingQuestions = [question3]
    , answeredQuestions = [question1]
    , answers = [answer1, answer2]
    }
type alias Model =
    { currentQuestion : Question
    , remainingQuestions : List Question 
    , answeredQuestions : List AnsweredQuestion
    }

type alias AnsweredQuestion = 
    { question : Question
    , answer : String
    }

Navigation

  • Display page according to initial URL
  • React to a click on a link
  • React to a URL change
-- http://my.url/
-- http://my.url/#game
-- http://my.url/#category/3

Program: Application

main : Program Value Model Msg
main = 
    Browser.application 
        { init = init
        , view = view
        , update = update
        , subscriptions = subscriptions
        , onUrlRequest = OnUrlRequest
        , onUrlChange = OnUrlChange
        }

type Msg =
    OnUrlRequest UrlRequest
    | OnUrlChange Url

Click on a link

New URL pushed

Application Key

init : Value -> Url -> Key -> ( Model, Cmd Msg )


update msg model =
    -- ...
    Navigation.pushUrl model.key url

OnUrlRequest & OnUrlChange

init : Value -> Url -> Key -> ( Model, Cmd Msg )


update msg model =
    -- ...
    Navigation.pushUrl model.key url
update msg model =
    case msg of
        OnUrlRequest (External url) ->
            (model, Navigation.load url)
update msg model =
    case msg of
        OnUrlRequest (External url) ->
            (model, Navigation.load url)

        OnUrlRequest (Internal url) ->
            (model, Navigation.pushUrl model.key (Url.toString url))
update msg model =
    case msg of
        OnUrlRequest (External url) ->
            (model, Navigation.load url)

        OnUrlRequest (Internal url) ->
            (model, Navigation.pushUrl model.key (Url.toString url))

        OnUrlChange url ->
            (changePage model url, Cmd.none)

Parsing the URL

import Url.Parser exposing (parsePath, s, </>, int)

parse (s "category" </> int) url
import Url.Parser exposing (parsePath, s, </>, int)

parse (s "category" </> int) url

-- /category/35/  ==>  Just 35
-- /category/42   ==>  Just 42
-- /              ==>  Nothing
-- /home          ==>  Nothing

There is a static string "category"

Then a slash

Then an int

Parsing the URL

type Route
  = Home
  | Category Int

routeParser : Parser (Route -> a) a
routeParser =
  oneOf
    [ map Home top
    , map Category (s "category" </> int)
    ]

myRoute = 
    parse route url

Main view

Msg

QuizPage

QuizPage view

Msg

QuizPage

Main Msg

No Runtime Exceptions

bug

bug

bug

bug

bug

bug

bug

bug

JavaScript

Border
Control

Messages

Ports : message to JS

port module Main exposing (main)

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    ( model, saveScore 5 )

port saveScore : Int -> Cmd msg
const app = Elm.Main.init({
  node: document.getElementById('maian')
});

app.ports.saveScore.subscribe(score => {
  localStorage.setItem('highscore', score);
});

Ports : message from JS

port module Main exposing (main)

type Msg = ScoreReceived Int

port scoreReceived : (Int -> msg) -> Sub msg

subscriptions : Model -> Sub Msg
subscriptions model =
    scoreReceived ScoreReceived
port module Main exposing (main)

type Msg = ScoreReceived Int

port scoreReceived : (Int -> msg) -> Sub msg

subscriptions : Model -> Sub Msg
subscriptions model =
    scoreReceived ScoreReceived

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
     -- ...
        ScoreReceived score ->
            ( { model | highscore = score }, Cmd.none )

Ports : message from JS

const app = Elm.Main.init({
  node: document.getElementById('maian')
});

const score = Number(localStorage.getItem('highscore'));

app.ports.scoreReceived.send(score);

Web components

view : Model -> Html Msg
view model =
    Html.node "analog-clock" 
        [ style "width" "200px"
        , style "height" "200px"
        , Html.Attributes.attribute "time" "1301757851000" 
        , Html.Events.on "ding" (Decode.succeed OnClockDinging)
        ] 
        []

Advantages

  • Great developer experience
  • No runtime exceptions
  • Great debugger
  • Powerful type system
  • Great performances
  • Great community

Drawbacks

  • Opinionated governance model
  • Boilerplate vs Explicitness
  • Not for every use case

Why use Elm?

  • Complex UI / Model
  • Frequent refactoring
  • You care for bugs
  • Easy introduction to FP
  • Write better code

Thank you!

Links

Slides : http://bit.ly/elm-university

Official website: elm-lang.org

The game: trivia-game.surge.sh

Workshop: github.com/jgrenat/elm-workshop 

Awesome talks:

 

University – Highway to Elm!

By ereold

University – Highway to Elm!

Slides for my university Highway to Elm!

  • 1,461