Diving into Elm

Written by on June the 1st, 2020.

The project

I mentionned on the first article of this series that I had an endgame project. I do not wish to reveal too many details about it, but I'm gonna introduce some background.

The project is to build a basic e-commerce website that sells only one product. But the product is a highly customizable one. Through a workflow made of questions with a fixed number of answers, the customer can customize his product. To keep it simple, and frankly to avoid losing the user into a maze of choices, the options you have at one step of the workflow are not affected by previous (or future ...) choices.

In order to complete the project, I need an API and a frontend. To ease the implementation of the frontend as a Single Page Application, I wanted to use a javascript framework, but it was already obsolete. All jokes apart, I chose to learn Elm.

What's Elm ?

Elm is a functionnal language that compiles to javascript. Together with its core libraries, the language allows you to build web applications.

Functionnal languages

A functionnal language does not have variables : it only has functions which are mathematical expressions that compute an output value solely based on input values. The input values cannot be updated by the function. The only effect of applying a function - as opposed to calling a procedure or method - is getting its return value. Forget about writing object oriented stuff such as setSomeField.

A functionnal language does not have loops or any form of code branching statements. Instead, every language construct returns a value. For example, Elm's if returns a value based on the truth of a boolean expression. So it's what you'd call a ternary expression in other languages.

The value returned by the if expression could be a function, but it does not mean that the execution path of your program changes. It rather means that the data will flow to this function instead of the one returned by the else statement (which is mandatory, because you must return a value in any case).

Data flow and execution flow might seem to be the same (and in the end, on the CPU, it all comes down to a sequence of execution steps), but while programming, "data flow" is a better way of thinking functionnal.

Elm's architecture

A Elm webapp is primarily made of three pieces of code :

At that point, if you wish to read some Elm code and have a complete introduction to the language, see the official guide.

Why Elm ?

A co-worker made a presentation about Elm some time ago. Since then, I was curious to use a functionnal language. After all, the basic operation of a webapp is to fetch a "data tree" (JSON) from a server, then transform this data tree to a "view tree" (HTML). This sounds very functionnal and the paradigm seems to fit well.

There are a lot of SPA frameworks and to-javascript transpilers. No matter what they tell you, there are no silver bullet. No solution fits every needs.

So the most important thing when it comes to a tech like Elm is not why but when to use it. In other words, does it fit my project ? I will give my 2 cents based on my short experience, first with the pain points, then with the magical benefits of Elm.

The pain

Note that it's not criticism (despite the title pain may suggest otherwise). I enjoy working with Elm, and I'm merely listing the surprises I had along the way. You may have other surprises. If you're used to pure functionnal programming, you may have no surprise at all.

Think different

First, the most painful thing to achieve is to think different. If, like me, your background is made of procedual and object oriented languages, then thinking purely functionnal is a big shift. I was already familiar with some functionnal techniques, and I'm using them where I think they fit best. But a purely functionnal language like Elm is a whole different beast and bears its learning curve.

I think that, as a beginner, the hardest, yet most important step is to define your app's model. The problem is, not knowing all the features, operators, functions of the language and core libraries and how they all fit together in this new functionnal paradigm, defining the right model for your project is not easy.

To quote the official guide, the best advice I can give is to not plan too much ahead. Before you write any line of code, you usually think that you have a good idea of the model for your app.

You might be used to plan all technical decisions (i.e what unit of code does what) around this model and the expected interactions with it. That's because in many languages and also, depending on your team, refactoring is hard - so you do your best to prevent its need. But growing and refactoring Elm code incrementally is easy.

We'll see later that you just can't shoot yourself in the foot. So do plan a bit the shape of your program, but not too much.

Syntax

Elm's syntax itself, though very simple, first felt very unusual to me. For example, it's not mandatory to specify the types of your function's arguments. But it's highly recommended to hint them with a funny arrow syntax :

			
myFunction : FirstInputArgumentType -> SecondInputArgumentType -> ReturnType
myFunction x y = x * y
			
		

Another surprise lies in the way you apply functions : the syntax uses spaces to separate the arguments. So if one of the arguments is another function, you must wrap it in parenthesis to delimit the inner function's arguments from the outer's :

			
myFunction1 x y = x * y
myFunction2 x = x * 2

myFunction3 x = myFunction1 x (myFunction2 x)
			
		

Updating the world

Then, there was updating records.Records are data structures with named, strongly typed fields. But records are not objects. There are no methods, and definitely no setters. What if you want to update a field ? You use a special syntax that produces a new record (an updated copy of the initial record) :

			
newModelFunction oldModel = { oldModel | name = newName }
			
		

The limitation of this syntax is that you cannot update a nested field :

			
newModel = { oldModel.customer | name = newName }
			 
		

This does not work. You can use awkward let expressions that let you name intermediary expressions :

			
let 
	oldCustomer = model.customer
	newCustomer = { oldCustomer | name = newName }
in
	{ oldModel | customer = newCustomer }
			
		

Searching for answsers, I found a better solution. Say you want to update your model's customer name with a new name, in response to a form field update. You will define some kind of update or sub-update function to treat the external update event. This external event will provide you the new value of the field.

With the help of the pipe operator, one can write :

			
updateName : Model -> String -> Model
updateName oldModel newName =
	newName
	|> asCustomerNameFor oldModel.customer
	|> asCustomerFor oldModel

asCustomerNameFor : Customer -> String -> Customer
asCustomerNameFor customer name = { customer | name = name }

asCustomerFor : Model -> Customer -> Model
asCustomerFor model customer = { model | customer = customer }
			
		

What happened ? The |> pipe operator simply applies the second operand (which must be a function, such as asCustomerNameFor) with the first operand (newName) as the last argument for that function. It just kinda flips an expression.

As you can chain and combine them like any other operator, and because the result of the first |> is a Customer, we can feed it as the last argument to the second |> asCustomerFor.

I must admit I like this little pattern very much. It's fun. It looks a bit like object-oriented setters, but more powerful. You can really see the data flow mentionned earlier. Because functions are values, you might even use pipelines to transform or wrap other functions. Be careful not to abuse it though !

Missing pieces ... and humans

Elm is not as famous or widely used compared to the big 3 (Angular, React and Vue). This comes with a few disadvantages :

But it's not all bad : Elm also has some great features baked in its core design. Let's take a look at them !

The magic

No runtime errors

Elm's homepage claims that there are no runtime errors in practice. Let's get something clear : you will get errors. But those will be external : the network will break, the browser will apply some sandboxing rule, and so forth.

The key argument is that the Elm compiler will not let you ignore those errors. Any error event must be treated like any other event, leading to a new model (that could be an error state, allowing the view to display an error message).

In fact, the Elm compiler does not allow you to ignore any possibilities. For example, null does not exist, but the core libraries defines a Maybe type.

Maybe can either be of value Nothing, or of value Just with a "payload" value of the type you'd expect. If you write an expression that uses a Maybe anywhere, the compiler will force you to return a value both when the value is nothing and when the value is something. This is known as pattern matching :

			
myFunction : Maybe String -> String
myFunction x =
	case x of
		Just str ->
			"the string is : " ++ str
		Nothing ->
			"sorry, but the string does not exist"

function2 = myFunction Nothing
function3 = myFunction (Just "hi")
			
		

You can (and you must) define your own variant data types like the Maybe. A variant type is a type that can hold any of the enumerated, named possible values. Here's a simple example of a "current page" type definition :

			
type Page
    = BuildStep Step
    | DeliveryForm Customer
    | Payment String
    | Loading
    | Error
			
		

For each possible value, it can hold another value of a defined type (the Step type of BuildStep Step above), or nothing. When you use values of those types, the compiler never let you ignore one of the possible value for the type. The standard library uses these variants types everywhere, so it covers all possible error or corner cases.

Divide by zero ? You will get the Infinity number. But hey, before it comes to that, validate your user input so you don't run into a zero divisor in your algorithm.

So, in practice, you have no runtime errors. You may have bugs if you made a mistake implementing the algorithm. But you will have error handling for every external problem such as non-20x HTTP responses. Elm's compiler is the good friend that keeps you away from the self-foot-shooting revolver.

Note that, despite all the efforts put into having friendly error messages, the compiler is not psychic. Don't get me wrong : the compiler outputs clear, readable and friendly errors messages, often with an example of a "smart" possible fix. It's a nice touch because when you start, you get a lot of these.

But because of the parser bias, the fix is not always the one you want to implement. It's all very normal since the compiler does not yet read into your mind. It just feels annoying sometimes to be offered an advice that doesn't fit your goal.

Let me add that compilers and language designs should all behave like Elm. This is because, the point of having computers is to make them do boring stuff we don't want to manage ourselves. Testing a program for its validity/correctness sounds like a computer problem, not a me problem.

Because if you have to do these checks yourself, chances are that you or an unaware coworker will miss something at some point - probably on one of those late-Friday evenings, just before a critical deployment. And if you rely on automatic tests, that means that anything the compiler does not check, you will need to write a test for it. Then test the test (no false positives, no false negatives). Then maintain the test.

My point is : any restriction or convention defined by the language should be checked ahead of time by a compiler or standard, SDK-bundled tool. Period.

Easy refactoring

Thanks to all the checks the compiler does, it's easy to rewrite and grow Elm code. This page states that it's okay to have bigger Elm files than what you're used to. I wrote all my app in a single 450 lines file, which I would not advocate for usually.

I tried to follow Elm's guide advice and avoid planning and splitting files too early. The model and other essential parts changed during implementation - it's a healthy part of the development process -, demanding small changes through the code. Splitting to files usually helps refactoring by scoping responsibility.

But in the case of Elm, those changes were very easy to (re) write, even with a simple text editor with no IDE-like features (I used Sublime Text without any Elm support except for the syntax highlighting). The changes you have to make to grow the code are not subtle enough to require the splitting to modules and defining contracts between them - to a point.

Indeed, because values are immutable (they are not variables), there's no hidden temporal coupling or hard-to-track mutations. The code does not usually lie, but it's especially true for Elm code.

Effortless clean code

Now, what do I mean by clean ? It just means that reading it - once you get the syntax - gives you the answer you need. There's no side effects to chase, check, or think of. The language design and suggested application architecture lead to a beautiful mathematical representation of your program.

Once you get your bearings in this new environment, writing code is a very abstract experience. You define the program as a relationship between different pieces of data and external actions, and it just writes itself. You don't have to think in terms of a sequence of steps to execute and branching flows that achieve your program's goals. You express what's what to what and let the framework do the rest.

Also, there's no HTML-JS-mixing templating language. You write your view using regular Elm syntax and the framework-provided functions. So you're not gonna awkwardly mix JS and HTML with some bizarre templating language. For example, a partial view function looks like :

			
deliveryForm : Customer -> Html Msg
deliveryForm customer =
	div [class "row"] [
	    div [class "column"] [
	        form [onSubmit PlaceOrder] [
	            label [for "firstName"] [text "Prénom"],
	            input [name "firstName", type_ "text", required True, value customer.firstName, onInput UpdateCustomerFirstName] [],
	            label [for "lastName"] [text "Nom"],
	            input [name "lastName", type_ "text", required True, value customer.lastName, onInput UpdateCustomerLastName] [],
	            label [for "email"] [text "Adresse Email"],
	            input [name "email", type_ "email", required True, value customer.email, onInput UpdateCustomerEmail] [],
	            label [for "postal"] [text "Adresse de livraison"],
	            textarea [name "postal", onInput UpdateCustomerPostal] [text customer.postal],
	            button [] [text "Commander !"]
	        ]
	    ]
	]
			
		

This is plain Elm code, in which you can add any amount of logic maths. Including ternary if's expressions, pattern matching for variant types, the standard list type's map function (transforming each item in a list to a HTML tag) ... and so forth.

Great debugger

One great hidden feature - hidden in the sense that it's not obviously advertised anywhere, or not as much as it should - is Elm's debugger. It's very simple, but helps you to find your mistakes - the one the compiler could not catch - very easily. It's available via the --debug command line switch of the compiler.

Once enabled, when you test your webapp on your favorite browser, a small icon at the bottom of the page is available. If you click on it, a popup appears. In the popup, you have the list of all the successive "states of the world" of your app. You see what event and what model lead to which new model, including user clicks, HTTP response, everything.

With that tool, you can trace back your bug to the actual update that lead the model to an unexpected state. Once you know wich event is responsible, you know exactly which piece of code is to be trashed needs fixing.

Expected, modern feature

Finally, any modern language should come bundled with :

Conclusion

Like everything, Elm's no silver bullet. It roots its powers in the functionnal paradigm. From there, a reliable framework let you write your web app easily, while keeping the code clean and easy to maintain. I've seen examples or used other SPA frameworks, and they were awkward to use, mixing JS and HTML in weird ways and not helping much to cope with Javascript's pitfalls.

But because of the lack of support and smaller community - past the great official guide -, the paradigm shift to the functionnal lands, and the increased complexity of interacting with native JS library - while possible - you may not want to introduce Elm at your day job. It all depends on your environment's mindset, on your team and the stakes of your project.

I would only use Elm at the day job if :

As for me, I don't regret my choice. It's both fun and reliable to work with Elm, and it fits the needs of my current project.