When the lead developer of a very interesting and innovative project suggested switching from AngularJS to Elm, my first thought was: Why?

We already had a nicely written AngularJS application that was in a solid state, well tested, and proven in production. And Angular 4, being a worthy upgrade from AngularJS, could have been a natural choice for the rewrite—so could React or Vue. Elm seemed like some strange domain-specific language that people have barely heard of.

Well, that was before I knew anything about Elm. Now, with some experience with it, especially after transitioning to it from AngularJS, I think I can give an answer to that “why.”

In this article, we will go through the pros and cons of Elm and how some of its exotic concepts perfectly suit the needs of a front-end web developer. For a more tutorial-like Elm language article, you can take a look at this blog post.

Elm: A Purely Functional Programming Language

If you are used to programming in Java or JavaScript and feel like it is the natural way of writing code, learning Elm can be like falling down the rabbit hole.

The first thing you will notice is the strange syntax: no braces, lots of arrows and triangles.

You may learn to live without curly braces, but how do you define and nest blocks of code? Or, where is the for loop, or any other loop, for that matter? While explicit scope can be defined with a let block, there are no blocks in the classic sense and no loops at all.

Elm is a purely functional, strongly typed, reactive, and event-driven web client language.

You may start to wonder if programming is even possible this way.

In reality, these qualities add up to give you an amazing paradigm of programming and developing good software.

Purely Functional

You might think that by using the newer version of Java or ECMAScript 6, you can do functional programming. But, that is just the surface of it.

In those programming languages, you still have access to an arsenal of language constructs and the temptation to resort to the non-functional parts of it. Where you really notice the difference is when you cannot do anything but functional programming. It’s at that point where you eventually start to feel how natural functional programming can be.

In Elm, almost everything is a function. The record name is a function, the union type value is a function—every function is composed of partially applied functions to its arguments. Even the operators like plus (+) and minus (-) are functions.

To declare a programming language as purely functional, rather than the existence of such constructs, the absence of everything else is of paramount importance. Only then you can start thinking in a purely functional way.

Elm is modeled upon mature concepts of functional programming, and it resembles other functional languages like Haskell and OCaml.

Strongly Typed

If you program in Java or TypeScript, then you know what this means. Every variable must have exactly one type.

Some differences exist, of course. Like with TypeScript, type declaration is optional. If not present, it will be inferred. But there is no “any” type.

Java supports generic types, but in a better way. Generics in Java were added later, so types are not generic unless otherwise specified. And, to use them we need the ugly <> syntax.

In Elm, types are generic unless specified otherwise. Let’s look at an example. Suppose we need a method which takes a list of a particular type and returns a number. In Java it would be:

public static <T> int numFromList(List<T> list){ return list.size(); }

And, in the Elm language:

numFromList list = List.length list

Although optional, I strongly suggest that you always add type declarations. The Elm compiler would never allow operations on wrong types. For a human, it’s a lot easier to make such a mistake, especially while learning the language. So the program above with type annotations would be:

numFromList: List a -> Int numFromList list = List.length list

It can seem unusual at first to declare the types on a separate line, but after some time it starts to look natural.

Web Client Language

What this means simply is that Elm compiles to JavaScript, so browsers can execute it on a web page.

Given that, it’s not a general purpose language like Java or JavaScript with Node.js but a domain-specific language to write the client portion of web applications. Even more than that, Elm includes both writing the business logic (what JavaScript does), and the presentational part (what HTML does)—all in one functional language.

All of these are done in a very specific framework-like way, which is called The Elm Architecture.

Reactive

The Elm Architecture is a reactive web framework. Any changes in the models are rendered immediately on the page, without explicit DOM manipulation.

In that way, it’s similar to Angular or React. But, Elm also does it in its own way. The key to understanding its basics is in the signature of the view and update functions:

view : Model -> Html Msg update : Msg -> Model -> Model

An Elm view isn’t just the HTML view of the model. It’s HTML which can produce messages of kind Msg , where Msg is an exact union type you define.

Any standard page event can produce a message. And, when a message is produced, Elm internally calls the update function with that message, which then updates the model based on the message and current model, and the updated model is again internally rendered to the view.

Event-driven

A lot like JavaScript, Elm is event-driven. But unlike, for example, Node.js, where individual callbacks are supplied for the async actions, Elm events are grouped in discrete sets of messages, defined in one message type. And, like with any union type, information that separate type values carry could be anything.

There are three sources of events that can produce messages: user actions in the Html view, execution of commands, and outer events to which we subscribed. That’s why all three types, Html , Cmd , and Sub contain msg as their argument. And, the generic msg type must be the same in all three definitions—the same one supplied to the update function (in the previous example, it was the Msg type, with a capital M), where all message processing is centralized.

Source Code of a Realistic Example

You can find a complete Elm web application example in this GitHub repository. Although simple, it shows most of the functionality used in everyday client programming: retrieving data from a REST endpoint, decoding and encoding JSON data, using views, messages, and other structures, communicating with JavaScript, and all that is needed to compile and package Elm code with Webpack.

The application displays a list of users retrieved from a server.

For an easier setup/demo process, Webpack’s dev server is used for both packaging everything, including Elm, and serving the user’s list.

Some functionality is in Elm, and some in JavaScript. This is done intentionally for one important reason: To show the interoperability. You probably want to try Elm to start, or gradually switch existing JavaScript code to it, or add new functionality in the Elm language. With the interoperability, your app continues to work with both Elm and JavaScript code. This is probably a better approach than to start the whole app from scratch in Elm.

The Elm part in the example code is first initialized with config data from JavaScript, then the list of users is retrieved and displayed in the Elm language. Let’s say we have some user actions already implemented in JavaScript, so invoking a user action in Elm just dispatches the call back to it.

The code also uses some of the concepts and techniques explained in the next section.

Application of Elm Concepts

Let us go through some of the exotic concepts of the Elm programming language in real-world scenarios.

Union Type

This is the pure gold of the Elm language. Remember all those situations when structurally different data needed to be used with the same algorithm? It’s always difficult to model those situations.

Here is an example: Imagine you are creating pagination for your list. At the end of each page, there should be links to previous, next, and all pages by their numbers. How do you structure it to hold the information of which link the user clicked on?

We can use multiple callbacks for previous, next, and page number clicks, or we can use one or two boolean fields to indicate what was clicked, or give special meaning to particular integer values, like negative numbers, zero, etc. But none of these solutions can model exactly this kind of user event.

In Elm, it’s very simple. We would define a union type:

type NextPage = Prev | Next | ExactPage Int

And we use it as a parameter for one of the messages:

type Msg = ... | ChangePage NextPage

Finally, we update the function to have a case to check the type of nextPage :

update msg model = case msg of ChangePage nextPage -> case nextPage of Prev -> ... Next -> ... ExactPage newPage -> ...

It makes things very elegant.

Creating Multiple Map Functions with <|

Many modules include a map function, with several variants to apply to a different number of arguments. For example, List has map , map2 , … , up to map5 . But, what if we have a function which takes six arguments? There is no map6 . But, there is a technique to overcome that. It uses the <| function as a parameter, and partial functions, with some of the arguments applied as mid results.

For simplicity, suppose a List only has map and map2 , and we want to apply a function that takes three arguments on three lists.

Here is how the implementation looks:

map3 foo list1 list2 list3 = let partialResult = List.map2 foo list1 list2 in List.map2 (<|) partialResult list3

Suppose we want to use foo , which just multiplies its numeric arguments, defined like:

foo a b c = a * b * c

So the result of map3 foo [1,2,3,4,5] [1,2,3,4,5] [1,2,3,4,5] is [1,8,27,64,125] : List number .

Let’s deconstruct what is going on here.

First, in partialResult = List.map2 foo list1 list2 , foo is partially applied to every pair in the list1 and list2 . The result is [foo 1 1, foo 2 2, foo 3 3, foo 4 4, foo 5 5] , a list of functions that takes one parameter (as the first two are already applied) and returns a number.

Next in List.map2 (<|) partialResult list3 , it is actually List.map2 (<|) [foo 1 1, foo 2 2, foo 3 3, foo 4 4, foo 5 5] list3 . For every pair of these two lists, we are calling the (<|) function. For example, for the first pair, it is (<|) (foo 1 1) 1 , which is the same as foo 1 1 <| 1 , which is the same as foo 1 1 1 , which produces 1 . For the second, it will be (<|) (foo 2 2) 2 , which is foo 2 2 2 , which evaluates to 8 , and so on.

This method can be particularly useful with mapN functions for decoding JSON objects with many fields, as Json.Decode provides them up to map8 .

Remove All Nothing Values from a List of Maybes

Let’s say we have a list of Maybe values, and we want to extract only the values from the elements that have one. For example, the list is:

list : List (Maybe Int) list = [ Just 1, Nothing, Just 3, Nothing, Nothing, Just 6, Just 7 ]

And, we want to get [1,3,6,7] : List Int . The solution is this one line expression:

List.filterMap identity list

Let’s see why this works.

List.filterMap expects the first argument to be a function (a -> Maybe b) , which is applied on elements of a supplied list (the second argument), and the resulting list is filtered to omit all Nothing values, and then the real values are extracted from Maybe s.

In our case, we supplied identity , so the resulting list is again [ Just 1, Nothing, Just 3, Nothing, Nothing, Just 6, Just 7 ] . After filtering it, we get [ Just 1, Just 3, Just 6, Just 7 ] , and after value extraction, it is [1,3,6,7] , as we wanted.

Custom JSON Decoding

As our needs in JSON decoding (or deserializing) start to exceed what is exposed in the Json.Decode module, we might have troubles creating new exotic decoders. This is because these decoders are invoked from the middle of the decoding process, e.g., within Http methods, and it’s not always clear what their inputs and outputs are, especially if there are a lot of fields in the supplied JSON.

Here are two examples to show how to handle such cases.

In the first one, we have two fields in incoming JSON, a and b , denoting sides of a rectangular area. But, in an Elm object, we only want to store its area.

import Json.Decode exposing (..) areaDecoder = map2 (*) (field "a" int) (field "b" int) result = decodeString areaDecoder """{ "a":7,"b":4 }""" -- Ok 28 : Result.Result String Int

The fields are individually decoded with the field int decoder, and then both values are supplied to the provided function in map2 . As multiplication ( * ) is also a function, and it takes two parameters, we can just use it like that. The resulting areaDecoder is a decoder which returns the result of the function when applied, in this case, a*b .

In the second example, we are getting a messy status field, which can be null, or any string including empty, but we know the operation succeeded only if it is “OK”. In that case, we want to store it as True and for all other cases as False . Our decoder looks like this:

okDecoder = nullable string |> andThen (\ms -> case ms of Nothing -> succeed False Just s -> if s == "OK" then succeed True else succeed False )

Let’s apply it to some JSONs:

decodeString (field "status" okDecoder) """{ "a":7, "status":"OK" }""" -- Ok True decodeString (field "status" okDecoder) """{ "a":7, "status":"NOK" }""" -- Ok False decodeString (field "status" okDecoder) """{ "a":7, "status":null }""" -- Ok False

The key here is in the function supplied to andThen , which takes the result of a previous nullable string decoder (which is a Maybe String ), transforms it to whatever we need, and returns the result as a decoder with help of succeed .

Key Takeaway

As can be seen from these examples, programming in the functional way may not be very intuitive for Java and JavaScript developers. It takes some time to get used to it, with a lot of trial and error. To help understand it, you can use elm-repl to exercise and check the return types of your expressions.

The example project linked to earlier in this article contains several more examples of custom decoders and encoders which might also help you understand them.

But Still, Why Choose Elm?

Being so different from other client frameworks, the Elm language is certainly not “yet another JavaScript library.” As such, it has a lot of traits which can be considered positive or negative when compared to them.

Let’s start with positive side first.

Client Programming Without HTML and JavaScript

Finally, you have a language where you can do it all. No more separation, and awkward combinations of their mixing. No generating HTML in JavaScript and no custom templating languages with some stripped-down logic rules.

With Elm, you have just one syntax and one language, in its full glory.

Uniformity

As almost all of the concepts are based on functions, and a few structures, the syntax is very concise. You don’t have to worry if some method is defined on the instance or the class level, or if it is just a function. They are all functions defined on the module level. And, there aren’t a hundred different ways for iterating lists.

In most languages, there is always this argument about whether the code is written in the language’s way. A lot of idioms need to be mastered.

In Elm, if it compiles, it’s probably the “Elm” way. If not, well, it certainly isn’t.

Expressiveness

Although concise, the Elm syntax is very expressive.

This is achieved mostly through the use of union types, formal type declarations, and the functional style. All of these inspire the use of smaller functions. At the end, you get code which is pretty much self-documenting.

No Null

When you use Java or JavaScript for a very long time, null becomes something completely natural to you—an inevitable part of programming. And, although we constantly see NullPointerException s and various TypeError s, we still don’t think that the real problem is the existence of null . It’s just so natural.

It becomes clear quickly after some time with Elm. Not having null not only relieves us from seeing runtime null reference errors over and over again, it also helps us write better code by clearly defining and handling all situations when we might not have the actual value, thus also reducing technical debt by not postponing null handling until something breaks.

The Confidence That It Will Work

Creating syntactically correct JavaScript programs can be done very quickly. But, will it actually work? Well, let’s see after reloading the page and testing it thoroughly.

With Elm, it is the opposite. With static type checking and enforced null checks, it needs some time to compile, especially when a beginner writes a program. But, once it compiles, there is a good chance that it will work correctly.

Fast

This can be an important factor when choosing a client framework. The responsiveness of an extensive web app is often crucial for the user experience, and thus the success of the whole product. And, as tests show, Elm is very fast.

Pros of Elm vs Traditional Frameworks

Most of the traditional web frameworks offer powerful tools for web app creation. But that power comes with a price: over-complicated architecture with lots of different concepts and rules on how and when to use them. It takes a lot of time to master it all. There are controllers, components, and directives. Then, there are the compilation and config phases, and the run phase. And then, there are services, factories, and all the custom template language used in provided directives—all those situations when we need to call $scope.$apply() directly to make the page refresh, and more.

Elm compilation to JavaScript is certainly also very complicated, but the developer is protected from having to know all the nuts and bolts of it. Just write some Elm and let the compiler do its work.

And, Why Not Choose Elm?

Enough with praising Elm. Now, let’s see some of its not-so-great side.

Documentation

This is really a major issue. The Elm language lacks a detailed manual.

The official tutorials just skim through the language and leave a lot of unanswered questions.

The official API reference is even worse. A lot of functions lack any sort of explanation or examples. And, then there are those with the sentence: “If this is confusing, work through the Elm Architecture Tutorial. It really does help!” Not the greatest line you want to see in the official API docs.

Hopefully, this will change soon.

I don’t believe Elm can be widely adopted with such poor documentation, especially with people coming from Java or JavaScript, where such concepts and functions are not intuitive at all. To grasp them, much better documentation with a lot of examples is needed.

Format and Whitespace

Getting rid of curly braces or parentheses and using white space for indentation could look nice. For example, Python code looks very neat. But for creators of elm-format , it wasn’t enough.

With all the double line spaces, and expressions and assignments split into multiple lines, Elm code looks more vertical than horizontal. What would be a one-liner in good old C can easily stretch to more than one screen in the Elm language.

This may sound good if you are paid by lines of code written. But, if you want to align something with an expression started 150 lines earlier, good luck with finding the right indentation.

Records Handling

It is difficult to work with them. The syntax to modify the field of a record is ugly. There is no easy way to modify nested fields or to reference arbitrarily fields by name. And if you are using the accessor functions in a generic way, there is a lot of trouble with proper typing.

In JavaScript, a record or object is the central structure which can be constructed, accessed, and modified in a lot of ways. Even JSON is just a serialized version of a record. Developers are used to working with records in web programming, so difficulties in their handling in Elm can become noticeable if they are used as the primary data structure.

More Typing

Elm requires more code to be written than JavaScript.

There is no implicit type conversion for string and number operations, so a lot of int-float conversions and especially toString calls are needed, which then requires parentheses or function application symbols to match the correct number of arguments. Also, the Html.text function requires a string as an argument. A lot of case expressions are needed, for all those Maybe s, Results , types, etc.

The main reason for this is the strict type system, and that can be a fair price to pay.

JSON Decoders and Encoders

One area where more typing really stands out is JSON handling. What is simply a JSON.parse() call in JavaScript can span hundreds of lines in the Elm language.

Of course, some kind of mapping between JSON and Elm structures is needed. But the need to write both decoders and encoders for the same piece of JSON is a serious problem. If your REST APIs transfer objects with hundreds of fields, this will be a lot of work.

Wrap Up

We have seen about Elm, it’s time to answer the well-known questions, probably as old as programming languages themselves: Is it better than the competition? Should we use it in our project?

The answer to the first question may be subjective, as not every tool is a hammer and not everything is a nail. Elm can shine and be a better choice over other web client frameworks in many cases while falling short in others. But it offers some really unique value that can make web front-end development a lot safer and easier than the alternatives can.

For the second question, to avoid answering with also the age-old “it depends,” the simple answer is: Yes. Even with all the disadvantages mentioned, just the confidence that Elm gives you about your program being correct is reason enough to use it.

Coding in Elm is also fun. It’s a totally new perspective for anyone who is used to the “conventional” web programming paradigms.

In real use, you don’t have to switch your entire app to Elm immediately or start a new one completely in it. You can leverage its interoperability with JavaScript to give it a try, starting with a portion of the interface or some functionality written in the Elm language. You will quickly find out if it suits your needs and then broaden its usage or leave it. And who knows, you may also fall in love with the world of functional web programming.