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:
- Make impossible states impossible by Richard Feldman
- The life of a file by Evan Czaplicki
University – Highway to Elm!
By ereold
University – Highway to Elm!
Slides for my university Highway to Elm!
- 1,440