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 request
const 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, undefined
address 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, Belfast

Declarative 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 11

Declarative 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.50

Safety 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