<VR>

  • Home
  • Talks
  • Podcast
  • Blog

Rethinking best practices

Published on Sep 01, 2021

🛫4 mins to read

  • elm
  • javascript
  • notes
  • talk
  • elm
  • videos

I have been looking into compile-to-JS languages off late and Elm, in particular. One thing that keeps coming up in several pieces of literature about Elm is the idea of rethinking best practices. This article is my take away and notes from a talk by Jamison Dance in 2016 React Conf titled Rethinking All Practices: Building Applications in Elm.

The whole video talks about why we should consider building applications in Elm. It's very interesting to go back in time and see these talks and how the language adoption and ecosystem have evolved since then.

Why Elm

The argument for Elm (and other languages that with stricter type systems) always starts with the common error seen in most JS code bases.

undefined is not a function

Take the following code example from the talk:

function foo(num) { if (num > 10) { return 'demo code is the best code'; } } console.log(foo(1).toUpperCase()); // Uncaught TypeError: Cannot read property 'toUpperCase' of undefined

The big underlying problem in this code is two-fold.

  • Implicit return of undefined when the if condition is not met
  • Lack of type checker that is able to look at the call to toUpperCase() and determine that it could potentially be called on something that's not a string
Bug tracking as a solutionThe way we have learned to deal with such a fundamental issue in the language is to track bugs using software like Sentry or Rollbar. However, the ideal situation would be to use computers to prevent these errors and not discover them in production AFTER shipping code to users.

Tools like eslint, flow (typescript wasn't spoken about in this talk) can catch these issues while authoring code.

Gradual type systems

As much as tools like Flow and Typescript help, there is a fundamental tradeoff they had to make. These type systems are designed to be gradual so it's easier to adopt and convert codebases slowly over to the new setup.

However, gradual type systems are optional by design. You can turn them off. It's a side effect of the design choice to allow users to optionally adopt it. Now that we have widespread adoption, this is a problem because you can have Typescript and still not have type safety. This means there is no guarantee and there is an escape hatch that's easy to use, especially when you're in a pinch.

Elm, on the other hand, guarantees no runtime errors because you cannot opt out of the type system.

Elm's compiler in action

Let's look at the same function in Elm

foo num = if num > 10 then "demo code is the best code"

results in the following error

Error: Compiler process exited with error Compilation failed Compiling ...-- UNFINISHED IF -------------------------------------------------- src/Main.elm I was expecting to see an `else` branch after this: 47| if num > 10 then "demo code is the best code" ^ I know what to do when the condition is True, but what happens when it is False? Add an else branch to handle that scenario! Detected problems in 1 module.

If you add an else case and attempt to return null

foo num = if num > 10 then "demo code is the best code" else null

Elm complains because there is no such thing as null in the language.

Error: Compiler process exited with error Compilation failed Compiling ...-- NAMING ERROR --------------------------------------------------- src/Main.elm I cannot find a `null` variable: 52| null These names seem close though: num not abs acos Hint: Read <https://elm-lang.org/0.19.1/imports> to see how `import` declarations work in Elm.

Changing to use the maybe type, this code becomes:

foo : Int -> Maybe String foo num = if num > 10 then Just "demo code is the best code" else Nothing

The type signature foo : Int -> Maybe String is optional but there's another talk I saw where we go into the benefit of having the type signature there and having it separate from the parameter naming.

Here's another function that calls the previous function foo and runs a switch statement on the possible return values

bar : Int -> String bar num = case foo num of Just str -> String.toUpper str

Notice the problem? We have skipped the else case from the original function here as well. But this is harder to detect because its one level of abstraction away from the original function. The elm compiler throws the following output:

Error: Compiler process exited with error Compilation failed Compiling ...-- MISSING PATTERNS ----------------------------------------------- src/Main.elm This `case` does not have branches for all possibilities: 57|> case foo num of 58|> Just str -> 59|> String.toUpper str Missing possibilities include: Nothing I would have to crash if I saw one of those. Add branches for them! Hint: If you want to write the code for each branch later, use `Debug.todo` as a placeholder. Read <https://elm-lang.org/0.19.1/missing-patterns> for more guidance on this workflow.

The correct version of this function is below

bar : Int -> String bar num = case foo num of Just str -> String.toUpper str Nothing -> "Welp"

This is a peek into the friendly nature of error messages as well as the exhaustive nature of the compiler combined with an expressive type system. The benefit is we don't run into cases where the programmer forgot to account for a non "happy-path" use case.

Designed for maintainability

As applications grow, they become harder to maintain in Javascript. The underlying problem is there are patterns in Javascript but nothing that force you to adhere to them. The React model is great but you don't have to adhere to it.

Elm architecture is very similar to how you would build a react / redux application (mainly because Redux was inspired by Elm). It's a new language, there's new syntax and a lot of learning when you begin but the good thing is "how do I build stuff" has already been figured out. There is only one pattern to build applications and that constraint gives you the freedom to focus on solving the problem than spending time picking a pattern or ensuring the entire team sticks to the same pattern, thereby making maintainability a breeze.

Pure functions

Elm only has stateless functions and the type system will force you to write stateless functions. You cannot introduce side effects unintentionally. If you have been working in pure functions, you know they are easier to test, reason about and reuse. They are composable and maintenance becomes a lot easier in the long run.

Immutable data structures

Elm only has immutable data and you cannot mutate variables. This makes it a lot easier than in Javascript where immutability is optional and a lot of times, you depend on a library like immer to achieve the effect. There is, however, some boilerplate involved. Even when these libraries are used, it is inevitable that someone doesn't adhere to the pattern because they are new or unaware or in a hurry. I have personally seen this happen in teams where you scale from a handful of people to several hundred people over the years, the codebase slowly gets worse over time.

That's the difference between having the freedom to follow any paradigm in Javascript vs only being able to program in one model. The language is designed to provide you this benefit without having to rely on conventions or additional tooling to enforce this aspect. Additionally, there is no fatigue in trying to figure out how to do things the right way since there is only one way to do things in the language. This brings us to the core differentiation between JS and Elm - constraints vs guidelines.

Constraints can guide you towards better design

JS prefers less constraints and values freedom. Elm frees you up from making these decisions, so you can focus on your problem domain instead. This is not to say there is no downsides to elm.

Downsides of Elm

Elm does best when it runs everything, which is hard to achieve in real world projects.

It is still early days and there was no server side rendering as of this talk. However, there're some chatter about prerendering and server rendering frameworks that I haven't looked into yet.

Production readiness

A lot of time has passed since this talk and Elm is most certainly production ready right now. But the thing I got out of this section of the talk is a confirmation of a sentiment I've heard in multiple places:

Production readiness is not binary. It is more of a sliding scale.

The talk ended with how the JS community seemed to be evolving towards what Elm already is.

Further reading

Built with passion...

React

Used mainly for the JSX templating, client-side libraries and job secruity.

Gatsby

To enable static site generation and optimize page load performance.

GraphQL

For data-fetching from multiple sources.

Contentful

CMS to store all data that is powering this website except for blogs, which are markdown files.

Netlify

For static site hosting, handling form submissions and having CI/CD integrated with Github.

Images

From unsplash when they are not my own.

..and other fun technologies. All code is hosted on Github as a private repository. Development is done on VS-Code. If you are interested, take a look at my preferred dev machine setup. Fueled by coffee and lo-fi beats. Current active version is v2.12.1.

</VR>