At Revelry, we build high-performance, resilient software that stands the test of time. A key part of how we achieve this is by using the Elixir functional programming language – but of course, because we write software for the web, we also have to write JavaScript as well (although LiveView has made this significantly less painful). We’ve also trained up more than a few JavaScript developers into Elixir developers. Sometimes, these engineers struggle at first to understand some functional programming concepts – in my opinion, because JavaScript provides too many escape hatches when trying to write exclusively in this paradigm. Here are five concepts that JavaScript developers can master by learning them in Elixir.
-
Immutability
In Elixir, all data is immutable. Operations that transform data always create a new copy, leaving the original untouched. Variables can be freely rebound, but there’s no reference passing going on. Consider this example where we can reassign variables to any data type in Elixir and in JavaScript:
# Elixir
my_free_variable = 1
my_free_variable = 2
my_free_variable = "thing"
my_free_variable = %{"thing" => true}
// JavaScript
let my_free_variable = 1;
my_free_variable = 2;
my_free_variable = "thing";
my_free_variable = {thing: true};
As you can see, our variable my_free_variable
can be freely rebound in both languages to different data and data types. Those JavaScripters among you are probably already rolling your eyes: but what about declaring variables with const
?! Then you can’t rebind it, and you have immutability!
// JavaScript - look ma, I'm immutable!
const company = {name: 'revelry'};
company = {name: 'Revelry'};
// ^ this operation is illegal and will not work
Not quite.
Rebinding is not the same as mutability. Mutability refers to the ability to modify the actual underlying data. This is easiest to demonstrate with a data structure such as a map in Elixir (or an object in JavaScript):
# Elixir
my_map = %{"thing" => "value"}
> %{"thing" => "value"}
Map.put(my_map, "thing", "my_new_value")
> %{"thing" => "my_new_value"} # a new map is returned
my_map
> %{"thing" => "value"} # the map hasn't changed
// JavaScript
thing.other = 1;
// thing will now be {thing: true, other: 1}
As you can see, in Elixir, the operation to change the map resulted in a new map and the underlying data inside `my_map`
remained unchanged. However, in JavaScript, the change operation changed (mutated) the underlying data. Not so immutable
In order to make an object or array immutable in JavaScript, you need to use Object.freeze
, which disallows the adding, editing, or deleting of any properties or methods on the given object. This approach drastically reduces unexpected bugs and makes code inherently more predictable and easier to reason about.
-
Pattern Matching
I remember when I was first learning programming we had an exercise where we would take a number as input, and based on the number, determine whether the corresponding time should be AM or PM. This is fairly simple, and in JavaScript it is fairly good at teaching control flow.
Let’s implement a completely stupid way of doing this to illustrate a point:
let hour = 14;
if (hour < 12) {
console.log("AM");
} else {
console.log("PM");
}
This works, but it doesn’t scale well, it’s easy to forget cases, and it doesn’t look great. In addition, it will fail silently. Now, let’s implement the same broken solution as above in Elixir, except using pattern-matching function heads:
def am_or_pm(12), do: IO.puts("It's midday!")
def am_or_pm(hour) when is_number(hour) and hour < 12, do: IO.puts("AM")
def am_or_pm(hour) when is_number(hour), do: IO.puts("PM")
am_or_pm("what time is it?") # 💥 BOOM
This looks a little better; we’re pattern matching on the hour
variable:
- When we call the function with 12, it outputs
"It's midday!"
. - When we call the function with a number greater than 12, it outputs
"AM"
- When we call the function with any other number, it outputs
"PM"
- When we call the function with a string as input, it explodes
This also isn’t very good and won’t work: but it will tell you! It doesn’t and can’t fail silently. If you don’t have a function clause that matches the input a function receives, it yells. LOUDLY. It also looks better, is extensible, and allows for more flexible paradigms that should be easier to read. if we really wanted to deal with unhandled input without exploding, we could add a fourth case that catches that as well.
We can get somewhat closer in JavaScript with switch(true)
(Although it’s worth noting that unless we add a default case, things will still fail silently.)
function isNumber(value) {
return typeof value === 'number';
}
switch (true) {
case hour === 12:
console.log("It's midday!");
break;
case hour < 12 and isNumber(hour):
console.log("AM");
break;
case isNumber(hour):
console.log("PM");
break;
}
This looks much more readable to me, and much more flexible.
-
Recursion!
A key paradigm shift for many JavaScript developers learning Elixir is that there are no ‘loops’ as you might typically think of them: there’s no for
loops, or while
loops, or do
loops, or for in
loops or any of that stuff. All loops are done using recursion. Elixir kindly provides you with abstractions for using that recursion (see Enum.map
, Enum.filter
, etc), but it is just recursion all the way down.
It’s important to note here that Elixir is optimized for recursion, and there’s no performance risk like there is in JavaScript. In JS, each recursive call adds a frame to the call stack. In contrast, Elixir (and Erlang, which Elixir is built on) optimizes tail-recursive calls – which means that if the last operation of the function is a call to itself, it reuses the current stack frame.
Elixir has what amounts to syntactic sugar for loops, but recursion feels much more natural in Elixir than it does in JavaScript. Especially when combined with pattern matching, building recursive loops and reasoning about them is much more straightforward:
def count_down(0), do: IO.puts("Done!") # function head one
def count_down(n) do # function head two
IO.puts(n)
count_down(n - 1)
end
count_down(3) # <- this causes the following: # count_down(3) hits function head two, n is now 2
# count_down(2) hits function head two, n is now 1
# count_down(1) hits function head two, n is now 0
# count_down(0) hits function head one
Done!
This functional approach leads to cleaner, more concise code that is often easier to reason about, test, and scale, resulting in a more reliable and extensible application for our clients.
-
Pipelines and transformations
The elegance of composing programs can be seen vividly in how the Unix operating system uses pipes. Brian Kernighan, one of the authors of ‘The C Programming Language,’ famously demonstrated this by executing a spell check not with a single function call, but by piping data through several distinct functions, with the result of each step flowing into the next.
# Elixir versionfile_contents |> get_words |> lowercase |> uniq |> sort |> look_up
This type of pipelining is something you can’t do natively in JavaScript. You either have to write your own pipe function, compose function calls, use a third-party library, or use intermediary variables.
// JavaScript
lookUp(sort(uniq(lowercase(getWords(fileContents)))));
# this is fine if you read right to left
The concept of taking data and transforming it as it moves through a pipeline can be extracted and applied to JavaScript programs. This makes them clearer and less prone to side effects. This compositional style results in highly predictable, maintainable, and testable code, significantly reducing bugs and accelerating the development of complex features for our clients (as well as making it easier to debug!)
-
Typechecking
Elixir is strongly and dynamically typed, so variables aren’t locked into being a specific data type, but certain operations, like adding a string and a number together, are prevented.
# Elixir
sum = "1" + 2
# ^ forbidden
// JavaScript
let sum = "1" + 2;
// Take a guess
It also allows developers to define types and typespecs. This is similar in some ways to TypeScript in that it’s something you can layer on top of your existing dynamically typed code. However, there is no dedicated file extension like .ts
in TypeScript that provides immediate, in-editor developer experience (DX) for type errors. The way typespecs are used is typically by a tool called Dialyzer, which performs the type checking that you’d get from something like TypeScript.
Conclusion
And that’s it! If you can master these concepts (and you’re already developing in JavaScript), the world is at your feet: your code will be cleaner, your bugs will be more apparent, and things will (hopefully) just make more sense.
We use Elixir and functional paradigms to build bulletproof software for our clients; you can too!