Fearless refactoring
with elm

by Daniel Bachler (@danyx23)

this presentation is available at:

slides.com/danielbachler/fearless-refactoring-with-elm

25 minutes

the language

the architecture

the infrastructure

my experience so far

 

my pain points with js

  • No static typing means simple typos can lead to runtime errors
  • Deep changes to data structures in non-trivial JS projects need increadible amounts of diligence and attention
  • A big part of the unit & integration tests test really boring properties (does it correctly reject an undefined param, ...)

What is elm?

  • a functional programming language with static type checking that is compiled to JS
  • an FRP (functional reactive programming) runtime
  • designed to be easy to learn and good for web frontend dev
  • uses VDOM to efficiently update the DOM, similar to React

Elm, the language

  • Pure functional
  • Statically typed
  • Beginner friendly
function addNumbers(a, b) {
    return a + b;
}


// Whatever
addNumbers(4, "three");
addNumbers a b =
    a + b

-- Types are infered to be numbers

-- This is a compile time error
addNumbers 4 "three"

super friendly and helpful compiler

some personal favourite features

Union types

-- Union Type

type Gender 
    = Male
    | Female
    | Other String

personAGender = Male

personBGender = Other "Androgynous"

printGender gender =
    case gender of
        Male ->
            "male"
        Female ->
            "female"
        Other selfdefined ->
            selfdefined

Non-nullable values

-- There is no null, no undefined

type alias Person =
    { firstName : String
    , lastName : String
    , yearOfBirth : Int
    }

-- not possible
personA = { firstName = "Lao"
          , lastName = "Tse"
          , yearOfBirth = null
          }

birthYearToString person =
    toString person.yearOfBirth
-- instead:

type alias Person =
    { firstName : String
    , lastName : String
    , yearOfBirth : Maybe Int
    }


personA = { firstName = "Lao"
          , lastName = "Tse"
          , yearOfBirth = Nothing
          }

birthYearToString person =
    case person.yearOfBirth of
        Nothing ->
            "Unknown"
        Just age ->
            toString age

all values are immutable

x = 1

x = 2 -- compile error

x = x + 1 -- compile error

y = x + 1 -- ok

static typechecking

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

birthYearToString : Person -> String
birthYearToString person =
    ...
  • The compiler always knows if your types check out or not
  • You can add Type Annotations so the compiler can tell you where you went wrong
  • Elm is always strongly typed, whether you add annotations or not (unlike Typescript, Flow, ...)
  • There is no "any" type like in TypeScript

 

All functions are pure

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

result1 = addNumbers 1 2
result2 = addNumbers 1 2
result 1 == result 2 -- True

Calling the same function again with the same arguments will always return the same value, never have any side effects

function addNumbersWeird(a, b) {
    window.myGlobalState = window.myGlobalState || 0;
    return a + b + (window.myGlobalState++);
}

var result1 = addNumbersWeird(1, 2);
var result2 = addNumbersWeird(1, 2);
result1 == result2; // False

fearless refactoring

  • Compiler nags until all refactoring bugs are fixed
  • Say goodbye to "undefined is not a function"
  • No runtime errors in Elm

Unit tests can focus on logic

  • In Javascript, unit tests have to check for undefined etc
  • In Elm, the type system takes care of all of this
  • Unit tests in Elm can focus on behaviour and logic
  • Pure functions are super nice to test (little mocking required)

The Elm Architecture

Very similar to React without local state + Redux

Diagram by Evan Czaplicki

The Elm Architecture

See also André Staltz' great overview of unidirectional UI aproaches

The Elm Architecture

-- MODEL

type alias Model = Int


-- UPDATE

type Action = Increment | Decrement

update : Action -> Model -> Model
update action model =
  case action of
    Increment ->
      model + 1

    Decrement ->
      model - 1


-- VIEW

view : Signal.Address Action -> Model -> Html
view address model =
  div []
    [ button [ onClick address Decrement ] [ text "-" ]
    , div [ countStyle ] [ text (toString model) ]
    , button [ onClick address Increment ] [ text "+" ]
    ]


countStyle : Attribute
countStyle =
  style
    [ ("font-size", "20px")
    , ("font-family", "monospace")
    , ("display", "inline-block")
    , ("width", "50px")
    , ("text-align", "center")
    ]

-- Wire everything together
main =
  start
    { model = 0
    , update = update
    , view = view
    }

benefits

  • Model is single source of truth
  • Creating visual elements is done by running the Model through a pure view function
  • Side effects (XHRs, ...) are modelled as Tasks, handled by the runtime, results come back into the app as actions
  • Apps are well structured. All state modifications happen in the central update (or a function called from there)
  • Easy to replay whole UI sessions (example)

The infrastructure

elm make

elm repl

elm package

package manager

  • Handles installation of packages
  • And publishing (inkl updates)
  • Automatically enforces SemVer (!!)

Interop with "native Javascript"

  • Incoming Ports (send messages from JS -> Elm)
  • Outgoing Ports (send messages from Elm -> JS)
  • Native modules (modules that use native JS - need to pass native review to be whitelisted)

Personal experience

  • Last spring, started work on a side project, a slideshow editor
  • Chose React for rendering (only pure components) with Redux to manage state (single source of truth) + Immutable.js
  • Many nice properties, but refactoring was still a pain. Also pretty verbose, immutable.js field access via strings, ...
  • Evaluated Typescript, Flow
  • Then took a closer look at elm and really liked it

+

  • General experience was very pleasant
  • Compiler is very helpful, many bugs can't even exist, code quality is higher
  • Refactoring with joy and confidence -> makes you do it more -> makes the code better
  • Community is friendly and helpful
  • Elm is small enough to learn it quickly, but still a very good stepping stone to other functional languages, esp. Haskell
  • Code reads well

-

  • Some rough edges (e.g. I use a script that deletes the compiler intermediate files before every compile because it would occasionally miss changed files)
  • Elm core team is small, some PRs to Docs etc linger for a long time (will hopefully get better with the new Elm Foundation)
  • Almost no support for running outside the Browser (=no server side rendering, ...)
  • Some trivial things in JS are a bit of a hassle in Elm, e.g. focusing a control (has to be done via port)

Would I do it again?

absolutely!

How to get started

  • try.elm-lang.org and  debug.elm-lang.org
  • elm-lang.org - great docs, read "The Elm Architecture"
  • Check out awesome-elm
  • try it for one widget of your app or a small side project
  • If you don't know any other functional languages, the first steps will be a bit rough ("What do you mean there are no loops?")
  • But it is very beginner friendly and a small, well-designed language
  • Used in production today by several companies

Should we do a hackathon style
meetup in Berlin?

Thank you!

by Daniel Bachler (@danyx23 on twitter)

Fearless Refactoring with Elm

By Daniel Bachler

Fearless Refactoring with Elm

Slides for a 25 min talk at berlinjs.org about Elm

  • 10,209