@JoGrenat #elmlang
@JoGrenat
null > 0 // false
null == 0 // false
null >= 0 // ... true
JS syntax size
Time
ES6
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 –
By Chris Williams
17K+ LoC
200K+ LoC
Functional programming is a way of writing programs by using pure functions and avoiding mutable data and side-effects
function getRandomValue() {
return Math.random();
}
let numbersList = [];
function addNumberToList(number) {
numbersList.push(number);
return number;
}
getRandomValue(); // 0.9613065251542985
getRandomValue(); // 0.5225738715986086
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);
numbers = [1, 2, 3]
-- ...
numbers.push 4
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
"Elm has a very strong emphasis on simplicity, ease-of-use, and quality tooling."
1.2.3
Nothing changed for the outside
Something was added
Something has changed
See also Building Trust: What Has Worked by Richard Feldman
and What is success? by Evan Czaplicki
2012
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!?
-- a single line comment
{- a
multiline
comment
-}
myString : String
myString = "Hello"
add : Int -> Int -> Int
add a b = a + b
myUser : { login : String, password : String }
myUser = { login = "Jordane", password = "passw0rd" }
updatedUser =
{ myUser | password = "S3cur3d" }
-- { login = "Jordane", password = "S3cur3d" }
let
in
twentyFour + sixteen
3 * 8 + 4 * 4
let
twentyFour =
3 * 8
sixteen =
4 * 4
in
twentyFour + sixteen
myNumbers =
[2, 3, 4]
allNumbers =
1 :: myNumbers
-- [1, 2, 3, 4]
case myNumber of
0 ->
"Zero"
1 ->
"One"
anythingElse ->
"Nor 0 nor 1"
case myNumber of
0 ->
"Zero"
1 ->
"One"
_ ->
"Nor 0 nor 1"
case myStringMaybe of
Just myString ->
"myStringMaybe contains: " ++ myString
Nothing ->
"myStringMaybe contains nothing"
myStringMaybe = Just "Hello"
"Hello"
case myList of
[] ->
"The list is empty"
firstElement :: tail ->
"First element: " ++ firstElement
myList = ["first", "second", "third"]
"first"
["second", "third"]
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"
module Map exposing (navigate)
import Random
import Html exposing (div, h1)
Elm online editor
VS Code
IntelliJ
SublimeText
Emacs
Brackets
Vim
myList : List Int
myList = [1, 2, 3, 4, 5]
squared : List Int
squared =
List.map (\x -> x * x) myList
-- [1, 4, 9, 16, 25]
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)
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]
Model
view()
update()
Html Msg
Elm Runtime
Msg
Html
Event
(new) Model
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
update : Msg -> Model -> Model
update msg model =
case msg of
Increment ->
{ model | counterValue = model.counterValue + 1 }
Decrement ->
{ model | counterValue = model.counterValue - 1 }
shuffleAnswers answers
-- ["Rouge", "Noir", "Blanc", "Vert"]
shuffleAnswers answers
-- ["Noir", "Blanc", "Vert", "Rouge"]
Ask the Elm Runtime to do it!
Model
view()
update()
Html Msg
Elm Runtime
Msg
Model
Model
Msg
Cmd Msg
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
Runtime
Generator Question
Cmd Msg
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
question
No Runtime Exceptions
bug
bug
bug
bug
bug
bug
bug
bug
bug
External World
Border
Control
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
}
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"
Ok result
Err reason
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"
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"]
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)
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
}
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
}
-- http://my.url/
-- http://my.url/#game
-- http://my.url/#category/3
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
init : Value -> Url -> Key -> ( Model, Cmd Msg )
update msg model =
-- ...
Navigation.pushUrl model.key url
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)
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
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
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);
});
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 )
const app = Elm.Main.init({
node: document.getElementById('maian')
});
const score = Number(localStorage.getItem('highscore'));
app.ports.scoreReceived.send(score);
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)
]
[]
Official website: elm-lang.org
The game: trivia-game.surge.sh
Workshop: github.com/jgrenat/elm-workshop
Awesome talks: