Should I Learn Elm If I Am a JavaScript Developer?

6,025 reads

Good friend of mine said I should learn Elm.

— “You’ll get much from it doesn’t matter if you’ll be using it or not.”

And that was exactly that kind of friend who no matter what he says always sounds convincing. Probably because of his white beard and blue eyes. Or may be because he is actually almost always right.

Elm is 4 year-old grown-up which transpiles into javascript and doesn’t have js fatigue of choosing the ‘right’ framework because actually it is a framework. It is completely functional and is a father of redux:

So I sacrificed several promising-to-be-good weekends to get a portion of insights and now happy to share them with you:

1. No runtime errors

I will start straight with the hardcore concepts.

You can forget about `NaN`, ‘Uncaught TypeError’ or ‘foo is undefined’ annoying error messages in Elm, because it supports static type checking which means that at compilation time it checks all of your code to make sure that all of the variables and inputs and outputs from functions match up. This means that if your function accepts only String there is no way you can pass anything else.

And although it is recommended to declare the types like:

reps : String -> Int

(reps is a function name, String is input type and Int is output type), it is not required, because Elm can automagically figure out all the types itself(which is called type inference) and show you appropriate error or success messages after compilation is finished.

2. Union types

One good trick commonly used in Elm — is Union type which allows you to define possible values for the weird or complex type. Say we have an Exercise type which can be one of the following strings: Pull-up or Push-up

type Exercise = Pull-up | Push-up

and want to make a function reps which returns number of reps based on the exercise:

reps : Exercise -> Int

reps exercise =

case exercise of

Pull-up ->

10

Push-up ->

15

What’s important here is that whenever we pass a Union variable as a parameter(which is exercise in this example) we are forced to use case expression. And what’s more important is that all the branches of that case expression are checked by compiler. Which means that:

if you forget to handle a case(say Muscle-up with 8 reps), you get an error at compile time.

with 8 reps), you get an error at compile time. if you mistype any case (say Psuh-up ) by accident, you get an error at compile time. Nice, huh?

3. Edge cases

And we are not finished with types yet.

Even for me who came to developer world after years of writing requirements specifications and who understand the importance of edge cases, sometimes difficult not to forget them in code. What if value is undefined or json format is incorrect or http request is timed out?

Elm takes care of this by introducing Maybe, Result and Task types which are special cases of already familiar Union types.

If you have optional field you’ll probably use Maybe:

type alias Sportsman =

{ name : String

, age : Maybe Int

}

which means here that age field can present one of 2 values: Nothing or Just Int —first is empty and second is non-empty value. And because it is Union type, any time you pass this var as a parameter you’re forced to use case expression and handle all the variations so you cannot avoid handling null/undefined case:

getReps : Sportsman -> Maybe Int

getReps sportsman =

case sportsman.age of

Nothing ->

Nothing



Just age ->

Just 12

Same approach is used for Result and Task types but instead of optional fields Task is used for sync operations like parsing json, and Result is used for async operations like html requests. Both of them can end up in one of the following values: Success for successful cases and Failure for error cases and same as with Maybe we’re always forced to handle both cases.

4. Package distribution

Another consequence of static typing is automatically enforced versioning.

This means that if we create a library and want to share it with others we don’t have to think about its version numbers every time we update our package. Elm will do it for us.

Starting from initial version 1.0.0 it will compare code changes for each of the consequent releases of our package and create new version number which does most accurately reflect the changes we made. 🌟

5. Redux

Redux is a well known pattern in Javascript nowadays originated in Elm. You can find it in React and Angular and I even recently used it in a jQuery project without any modern MV* frameworks.

The idea is you have one single point of trust which is called Model (Elm) or State(React) or Store(Angular) and you have an interface to update it. In Elm we call this interface Update (which is Reducer in Javascript world).

So we call Update function each time we want to change the Model .

Update function gets current Model and Msg (Action) as parameters and returns the new Model .

Lately we use Model to update our view which is simply Html if we are talking about website, and we do it by calling the self-titled function — View .

That’s how it looks:

Elm architecture

And why is it cool?

6. QA and support

First of all QA becomes much-much easier.

We already know Elm is a functional language which means it provides us 2 functional guarantees:

all functions always return the same result given the same argument values

all functions don’t create side effects, i.e. don’t change anything outside their own scope

And basically this means that having a list of Update function calls we always can rerun them and will always get the same Model as a result of rerun. Why? Because no one can change our Model from outside(remember? no side effects) and because our Update function always returns the same result if called with the same parameters.

And this means that any time your QA engineer finds a bug she sends you the log of Update function calls(which is silently recorded) without manually documenting all the steps. And you instead of manually reproducing all the steps just import it in your app and immediately see the bug on the screen with possibility to move back and forth in the history of actions to see which chain of user actions led to the error. 🌟🌟

6. Lazy loading

Second fruit of using redux is lazy loading.

What I like about modern javascript frameworks comparing to jQuery is they are declarative. You don’t say in your program how to manipulate the DOM — append, remove, hide, etc. You just describe initial and final states of html — how it should look in different situations, and framework takes care(normally with Virtual DOM) of everything required to update the page so it looks like you want it to.

Same for Elm. You simply define the View function describing how html shall be rendered based on the current Model . And then when Model is changed your View function updates the DOM accordingly comparing current and previous states and making corresponding DOM manipulations.

Now interesting thing: what if only small part of the Model was updated and rest of it stayed unchanged? Does it make sense to recalculate and compare the whole DOM? Surely not.

If we break our view function into pieces so that one function renders header, another renders footer and etc then because of the first function guarantee(always return the same result given the same argument values), we can skip the functions whose input wasn’t changed.

We just put lazy keyword in front our function:

viewHeader: String -> Html Msg

viewHeader name =

...some code

view : Model -> Html Msg

view model =

lazy viewHeader model.loggedUser.name

...other code

Here we have a header which shows logged in user name. Once something is changed in the model(user scrolls down and new content is loaded) we don’t want to recalculate header part of the DOM because it is basically same as before. But we want to update content part, because user scrolled it. That’s what lazy do for us — Elm will check that viewHeader input wasn’t changed and skip this part completely but will render the rest. This saves some CPU resources for us and makes rendering faster.

7. Immutability

And this leads to another important concept of Elm which is immutability. In previous example we had String as an input for our Header function but what if we had something more complex — Object ?

What if we want to compare 2 input objects(new and old)? Then we have to go deep into their fields hierarchy and compare fields one by one. Which is time and resource consuming.

On the other side with immutability which requires to create new object each time we change something in it, comparison is as simple as comparing two references which is blazingly fast. Because if references point to the same object then it is the same object and if not then these are two different objects. Cool. Now lazy loading makes even more sense.

8. Even faster

But we can do even faster.

We all know that typical browser performs repaint 60 times per second as this is how our eyes perceive visual information. There is no need to do repaint more often, because we won’t improve any website look and feel but will be wasting device resources.

At the same time we want other operations apart from repaint to work in normal mode either it is an http request or some complex calculations in the background.

So how we can achieve this in Elm?

Well, we actually don’t have to do anything. It is built-in. Elm uses requestAnimationFrame under the hood to sync view calculations with browser repaints. This means that view function which renders html will not be called more often than 60 times per second and it saves time, CPU resources and mobile device battery.

The main reason we can do this in Elm is second function guarantee applied to the View function — it doesn’t have any side effects so the only thing it alters — is html. Which means it cannot update the Model . And while we make View function calls to happen only 60 times per second, our Model still gets updated in real-time without any delays. That’s the purity of pure functions.

9. Fuzz testing

As we already can see Elm spends a lot of efforts to prevent errors in our applications so the testing framework used in it is quite elaborate too.

One feature I’m excited about and which is coming soon in Mocha is fuzz testing.

Idea behind it is running the test case many or several times with randomly generated input. Cool part is all edge cases like zero, empty or negative values are guaranteed to be checked at least once and you can also set some ranges along with their probabilities to be sure that the most important cases are tested more often than others.

Fuzz.frequency

[ ( 1, Fuzz.intRange -100 -1 )

, ( 3, Fuzz.intRange 1 100 )

]

This can save a lot of manual test writing time but has it’s caveat that tests execution time can be noticeably increased.

Conclusion

So once we know all these cool things, how can we use them in our normal Javascript life?

First, use static typing— Typescript or Flow could be great solutions for that. Typescript allows you to mix it with javascript in the same code base which means you can gradually move your project into static typed without breaking legacy code. It is also provides great IntelliSense support in combination with VSCode.

Second — use functional approach where appropriate(state management, rendering, etc). Redux library is a great example.

Third, use immutability with help of ready to use libraries like Mori or immutable.js to advance change detection and improve application performance.

Tags