Slaying UI Anti-patterns with Elm
About me
Martin McKeaveney
- Software Engineer @Rapid7
- https://github.com/mmckeaveney

Agenda
-
The modern Front End Stack
-
What is Elm?
-
Mutable, Global State
-
Imperative Logic
-
Runtime errors and Unsafe APIs
-
Elm and the DOM
-
Tooling and architecture
-
Demo
Javascript Today
- Huge numbers of tools, frameworks and libraries
- Ubiquitous
- Popular across the full stack
- Moving at a ridiculous pace

- Javascript Fatigue?
- Different paradigms
- Applications getting bigger
- More logic moving to the client


- No runtime errors in practice. No null. No undefined is not a function.
- Friendly error messages that help you add features more quickly.
- Well-architected code that stays well-architected as your app grows.
- Automatically enforced semantic versioning for all Elm packages.
Functions


Purity & Immutability

Pure & Immutable
bill = { name = "Bill", age = 1 }
birthday person = { person | age = person.age + 1 }
birthday bill
-- { name = "Bill", age = 2 } : { name : String, age : number }
birthday bill
-- { name = "Bill", age = 2 } : { name : String, age : number }
birthday bill
-- { name = "Bill", age = 2 } : { name : String, age : number }
birthday bill
-- { name = "Bill", age = 2 } : { name : String, age : number }
birthday bill
-- { name = "Bill", age = 2 } : { name : String, age : number }const person = { name: 'Billy', age: 1 };
const birthday = (person) => {
person.age += 1;
return person;
}
birthday(person);
birthday(person);
birthday(person);
birthday(person);
birthday(person);
console.log(person);
/* {
age: 6,
name: "Billy"
} */
Referencial Transparency
Pure & Immutable
getProfile username =
let
url =
"https://api.github.com/users/" ++ username
request =
Http.get url userDecoder
in
Http.send LoadUser requestconst getProfile = (username) =>
fetch(`https://api.github.com/users/${username}`)
.then(res => res.json())
.then(console.log);Declarative vs Imperative

Declarative Programming
const request = require("request")
const comicsSeriesWithHero = (heroId) =>
request(`${MARVEL_BASE_URL}/characters/${heroId}`,
(error, response, body) => {
try {
if (!error && body) {
JSON.parse(body).data.results.forEach(({ id, title, description }) =>
allComicStories.push({ id, title, description }));
numberOfHeroRequests++;
if (numberOfHeroRequests == numHeroes) {
findIntersection();
}
} else {
console.error(error);
}
} catch (e) {
console.error(e);
}
}); 
Declarative Programming
function squareNumbers(numbers) {
const results = [];
numbers.forEach(num => results.push(Math.pow(num, 2)));
return results;
}
console.log(squareNumbers([1, 2, 3, 4, 5]));
// [1, 4, 9, 16, 25]squareNumbers = map ((^) 2)
squareNumbers [1,2,3,4,5]
-- [1, 4, 9, 16, 25]square n = n ^ 2
squareNumbers numbers = map square numbers
squareNumbers [1,2,3,4,5]
-- [1, 4, 9, 16, 25]squareNumbers numbers = map (\n -> n^2) numbers
squareNumbers [1,2,3,4,5]
-- [1, 4, 9, 16, 25]Declarative Programming
Currying
TLDR; - Don't evaluate a function until all arguments are passed
const address = (number, street, city) => {
console.log(`Your address is ${number} ${street}, ${city}`);
}
address("4", "sesame street");
// Your address is 4 sesame street, undefinedaddress number street city =
"Your address is " ++ number ++ " " ++ street ++ "," ++ city
address "6" "sesame street"
-- returns a Function that takes a city
address "6" "sesame street" "Belfast"
-- Your address is 6 sesame street, BelfastDeclarative Programming
Currying
findUser dbUrl userId =
Decode.list (userDecoder)
|> Http.get dbUrl ++ userId
|> Http.send UpdateUser
findUserMongo = findUser "http://urltomongodb"
-- Returns a function that takes a userId
findUserMongo 1
-- Returns the user from mongoDB with the id of 1
add x y = x + y
addOneTo = add 1
-- Returns a function that takes another int
addOneTo 10 -- All arguments now passed, evaluates to 11Declarative Programming
Piping / Forward Function Application
-- These are equivalent
viewNames names =
String.join ", " (List.sort names)
betterViewNames names =
names
|> List.sort
|> String.join ", "
-- These are also equivalent
squareNumbersUnder10 nums = map ((^) 2) (filter (\n -> n < 10) nums)
squareNumbersUnder10 [90, 100, 4, 2] -- [16,4]
squareNumbersUnder10 nums =
nums
|> filter (\n -> n < 10)
|> map ((^) 2)
squareNumbersUnder10 [90, 100, 4, 2] -- [16,4]
Safety & Strong Static Typing

Safety with Types
Elms Type System
-- Type Signatures
helloWorld : String
helloWorld = "Hello World!"
isFalse : Bool
isFalse = False
isCool : String -> String
isCool name = name ++ " is cool."
multiply : Int -> Int -> Int
multiply x y = x * y
listOfInts : List Int
listOfInts = [1, 2, 3]
hasOverNChars : String -> Int -> Bool
hasOverNChars str n =
(String.length str) > n
hasOverNChars : String -> Int -> Bool
hasOverNChars str n =
(String.length str) > n
hasOverNChars "NI Dev Conf" "5"
-- Error in compiler
The 2nd argument to function hasOverNChars is causing a mismatch.
Function hasOverNChars is expecting the 2nd argument to be:
Int
But it is:
String
Hint: I always figure out the type of arguments from left to right. If an
argument is acceptable when I check it, I assume it is "correct" in subsequent
checks. So the problem may actually be in how previous arguments interact with
the 2nd.
listOfInts : List Int
listOfInts = [1, 2, "dog"]
listOfInts
-- Compiler error
The 2nd and 3rd entries in this list are different types of values.
The 2nd entry has this type:
number
But the 3rd is:
String
Hint: Every entry in a list needs to be the same type of value. This way you
never run into unexpected values partway through. To mix different types in a
single list, create a "union type" as described in:Safety with Types
Records and Type Aliases
-- Good
bill : { name : String, bio : String }
bill = { name = "Bill", bio = "Hey, I'm bill" }
hasBio : { name : String, bio : String } -> Bool
hasBio user =
not (String.isEmpty user.bio)
hasBio bill
-- False
-- Better
type alias User =
{ name : String
, bio : String
}
bill : User
bill = User "Bill" "Hey, I'm bill"
hasBio : User -> Bool
hasBio user =
not (String.isEmpty user.bio)
hasBio bill
-- False
-- Some more examples
type alias ProgrammingLanguage =
{ name: String,
, creator: String
, yearCreated: Int
}
type alias SlushPuppie =
{ size: String
, color: String
, price: Float
}
javascript : ProgrammingLanguage
javascript = ProgrammingLanguage "Javascript" "Brendan Eich" 1995
largeRedSlushPuppie : SlushPuppie
largeRedSlushpuppie = SlushPuppie "Large" "Red" 2.50Safety with Types
Union Types
-- Slush Puppie / Color
type Color
= Red
| Blue
| Green
| Yellow
| Mix Color Color
type Size
= Small
| Medium
| Large
type alias SlushPuppie =
{ size: Size
, color: Color
, price: Float
}
redLargeSlushPuppie : SlushPuppie
redLargeSlushPuppie = SlushPuppie Large Red 2.50
smallGreenSlushPuppie : SlushPuppie
smallGreenSlushPuppie = SlushPuppie Small Green 1.50
partyTime : SlushPuppie
partyTime = SlushPuppie Large (Mix Red Blue) 2.50
-- Using Maybe
type Maybe
= Just a
| Nothing
type alias RemoteData =
{ response : Maybe (List User)
}
showNumberUsers : RemoteData -> String
showNumberUsers userData =
case userData.response of
Just users ->
"You have " ++ (length users) ++ " users"
Nothing ->
"No users found"
-- If we leave out one of the branches
This case does not have branches for all possibilities.
You need to account for the following values:
Maybe.Nothing
Add a branch to cover this pattern!
If you are seeing this error for the first time, check out these hints:
The recommendations about wildcard patterns and Debug.crash are important!
type RemoteData e a
= NotAsked
| Loading
| Failure e
| Success a
type alias Model =
{ users : RemoteData Http.Error (List User)
}
showNumberUsers : RemoteData -> String
showNumberUsers userData =
case userData of
NotAsked ->
""
Loading ->
"Loading users.."
Failure err ->
"Something went wrong"
Success users ->
"You have " ++ (length users) ++ " users"
Elm and the DOM


Elm and Virutal DOM
The Virtual DOM

- Faster Virtual DOM benchmarks than React
- Immutability and Purity allow this
Elm and Virutal DOM
Elm App Example
-- Read more about this program in the official Elm guide:
-- https://guide.elm-lang.org/architecture/user_input/buttons.html
import Html exposing (beginnerProgram, div, button, text)
import Html.Events exposing (onClick)
main =
beginnerProgram { model = 0, view = view, update = update }
view model =
div [ class "app-container" ]
[ button [ onClick Decrement ] [ text "-" ]
, div [] [ text (toString model) ]
, button [ onClick Increment ] [ text "+" ]
]
type Msg = Increment | Decrement
update msg model =
case msg of
Increment ->
model + 1
Decrement ->
model - 1
Tooling and TEA (The Elm Architecture)

Tooling and TEA
Tooling
Compiler/Bundler
REPL
Dev server
Package Manager




Tooling and TEA
The Elm Architecture
The logic of every Elm program will break up into three cleanly separated sections
- Model — the state of your application
- Update — a way to update your state
- View — a way to view your state as HTML

Tooling and TEA
Thanks for listening!
Questions?

- https://www.elm-tutorial.org/en/
- https://ellie-app.com/
- http://elm-lang.org/blog/blazing-fast-html
- http://elm-lang.org/docs
- https://guide.elm-lang.org/
Slaying UI Anti-patterns with Elm
By Martin McKeaveney
Slaying UI Anti-patterns with Elm
- 823