Elixir Idioms for JavaScript Developers
When I set out to write this blog post, it was initially titled “Elixir Cheatsheet for JavaScript Developers.” Halfway through writing the post, I realized I had more than seven pages worth of content — far from a cheatsheet. There were so many interesting points of comparison and difference between Elixir and JavaScript, and it was difficult for me to prioritize what ought to go on the cheatsheet and what I could edit out.
With that in mind, I decided to write a different post: one with a narrower scope and hopefully less content. So in this post, I’ll focus on some interesting idioms I’ve picked up while developing web applications using both Elixir and JavaScript.
Prerequisites
This post assumes prior knowledge of both Elixir and JavaScript (Node.js or browser is fine). More specifically, it assumes familiarity with Elixir and proficiency with JavaScript. You can pick up the fundamentals of these languages by referring to the following resources:
What Are Idioms in Programming?
But first, let’s cover what idioms in programming are. An idiom is the usual way to accomplish a task in a given programming language. There are often many different ways to accomplish the same thing in a programming language. For example, there are at least four different ways to loop through the items of an array in JavaScript. An idiom in this context could be “the one way” to loop through items of an array that’s ubiquitous in the JavaScript community. Programming language idioms aren’t quite that straightforward, but that’s the idea. For a more thorough explanation, refer to this guide on programming idioms.
Before I began programming with Elixir, I spent more time programming with JavaScript, and in the process, I picked up the “JavaScript Way” of achieving some programming tasks. After using Elixir for the past few months, I’ve begun to pick up the “Elixir Way” of achieving certain programming tasks. Elixir is a functional programming language, which means that there are some ways of doing things — i.e. idioms — with Elixir (and perhaps with other functional programming languages) that are a direct consequence of Elixir’s functional nature. The next sections will explore some of them.
Dealing with Nested if Statements
Nested if
statements can quickly become unwieldy and difficult to read. One way I dealt with this in JavaScript was by using the “Return Early” technique I first read about in Tonya Mork’s book entitled Refactoring Tweaks.
So in JavaScript, we could start with code like this:
function aFunction(x, y, a, b) { if (y > x) { if (a == 'cars') { if (b.length == 6) { console.log(b); // Some more code. } } } }
And we could turn it into code like this:
function aFunction(x, y, a, b) { if (y < x) { return; } if (a !== 'cars') { return; } if (b.length == 6) { return; } console.log(b); // Some more code. }
This example is contrived, because it isn’t necessarily a realistic or practical way to go about achieving the goals of the code; it could be much simpler. But hopefully it illustrates the point. The “Return Early” technique makes it easier to follow the flow of the code, and it can make an unreadable tangle of nested code much easier to read.
However, this technique isn’t available in Elixir because Elixir doesn’t have a return
keyword like JavaScript does. Instead, in Elixir, the with
statement could be used to untangle nested if
statements.
So we’d start with Elixir code like this:
def a_function(x, y, a , b) do if y > x do if a == "cars" do if len(b) == 6 do IO.inspect(b, label: "B") # Some more code. end end end end
And we’d end up with code like this:
def a_function(x, y, a , b) do with true <- y > x, true <- a == "cars", 6 <- len(b) do IO.inspect(b, label: "B") # Some more code. end end
Again, this example is contrived, and with
isn’t the best way to detangle the function in this specific example, but I hope it illustrates the point.
While it’s not always possible, for this specific example, a better choice would be to use Elixir’s pattern matching facilities, along with multiple function clauses, like this:
def a_function(x, y, "cars" = a, b) when y > x do if len(b) == 6 do IO.inspect(b, label: "B") # Some more code. end end def a_function(x, y, a, b) do # Alternative code flow. end
Error Handling
In JavaScript, error states are usually represented with raised exceptions that are then easy to catch and handle. This means that code like this is possible:
function firstCall(x, y) { if (x > y) { throw new Error('Must be lesser'); } // Some more code. } function secondCall(x, y) { if (x < y) { throw new Error('Must be greater'); } // Some more code. } function doStuff(x, y) { try { firstCall(x, y); secondCall(x, y); } catch (error) { // Error handling logic. } }
In this example, doStuff
can call both functions and handle whatever error is thrown from either function.
Similar code is possible in Elixir using exceptions combined with try
, catch
, and rescue
statements. However, exceptions aren’t the only — or even the most common — way to represent error states in Elixir.
Usually, in Elixir, a function will return a two-element tuple, with the first element being an atom with a value of either :ok
or :error
. The error or valid states will be represented by the first element of the tuple, while the actual return value from the function will be in the second element of the tuple.
Error handling for situations like this is usually accomplished using the case
statement:
def first_call(x, y) do value = %CustomStruct{} if x > y do {:error, "Must be lesser"} else {:ok, value} end end def second_call(x, y) do value = %AnotherCustomStruct{} if x < y do {:error, "Must be greater"} else {:ok, value} end end def do_stuff(x, y) do case first_call(x, y) do {:ok, first_value} -> case second_call(x, y) do {:ok, second_value} -> IO.inspect(first_value) IO.inspect(second_value) # Code to run when both calls return `:ok`. {:error, error} -> # Error handling logic for `second_call`. end {:error, error} -> # Error handling logic for `first_call`. end end
In this example, both first_call
and second_call
return tuples, with the first element being either :ok
or :error
. The do_stuff
function examines the return value of first_call
and second_call
to determine if the calls were successful or if an error occurred. If either call returns an error, the appropriate error handling logic is executed.
The do_stuff
function in this example is a bit convoluted with the use of nested case
statements. Elixir’s with
statement can be used to handle these errors more cleanly and idiomatically:
def do_stuff(x, y) do with {:ok, first_value} <- first_call(x, y), {:ok, second_value} <- second_call(x, y) do IO.inspect(first_value) IO.inspect(second_value) else {:error, error} -> # Error handling logic. end end
In this example, the with
statement is used to chain the correct :ok
states of both the first_call
and second_call
functions. The code in the do
block is only executed if both functions are in an :ok
state. The :error
state for both calls is then handled in the else
block.
Pattern Matching
We used pattern matching earlier when discussing techniques for handling nested if
statements, but it’s worth reiterating this concept in its own section.
Pattern matching is a powerful concept in Elixir that can be used to simplify code and make it more readable. Instead of using if
statements, Elixir developers can take advantage of pattern matching to handle different cases. For example, instead of using nested if
s to check the validity of a value, we can use pattern matching to match against different cases, making the code more readable:
def a_function(x, y, "cars" = a, b) when y > x do if len(b) == 6 do IO.inspect(b, label: "B") # Some more code. end end def a_function(x, y, a, b) do # Alternative code flow. end
This example uses pattern matching against the value of a
to check if it equals "cars"
, instead of using nested if
statements. This approach can make the code more readable and easier to understand.
Chaining Function Calls with the Pipe Operator
In JavaScript, it’s possible to pass the output of a function into another function. An example could be this code:
// JavaScript function multiply(x, y) { return x * y; } function add(x, y) { return x + y; } function math(x, y) { return multiply(add(x, y), x); }
In the example above, the result of add
is passed to multiply
. When we’re working with methods of an object, instead of functions, like is the case when dealing with arrays, we can also chain method calls together in JavaScript, like this:
const exampleArray = [1, 2, 3, 4, 5]; exampleArray .filter((x) => x % 2 === 0) .map((x) => x * x) .forEach((x) => console.log(x));
We can have similar code in Elixir:
def multiply(x, y) do x * y end def add(x, y) do x + y end def math(x, y) do multiply(add(x, y), x) end
However, Elixir has a powerful operator called the pipe operator (|>
) that can make nested function calls like this more readable:
def multiply(x, y) do x * y end def add(x, y) do x + y end def math(x, y) do add(x, y) |> multiply(x) end
The pipe operator will pass the output of the first function call as the input for the first argument to the next function in the chain.
Elixir doesn’t have methods or objects like JavaScript does. Instead, in Elixir, there are functions and modules. To make it easier to “pipe” functions of a module, it’s common that, if a module contains functions that all operate on the same data structure, the first parameter for all the functions in that module will be the given data structure. For example, the functions of Elixir’s Enum
module all expect an Enum
as their first argument.
This makes it possible to use the pipe operator to write readable code like this:
list = [1, 2, 3, 4, 5] list |> Enum.filter(fn x -> rem(x, 2) == 0 end) |> Enum.map(fn x -> x * x end) |> IO.inspect #=> [4, 16]
In this example, we define a list of numbers and then use the pipe operator to filter out the odd numbers, square the remaining even numbers, and then print the result.
Conclusion
This post covered a few of the more interesting idioms in Elixir, approaching them from the point of view of a JavaScript developer in hopes of providing newcomers a helpful guide to understanding Elixir’s paradigms.
Elixir is a powerful and expressive language that can be used to build robust and scalable applications. With its support for functional programming, concurrency, and fault-tolerance, Elixir is a great language to add to your toolkit.
Rukky joined Nutrient as an intern in 2022 and is currently a software engineer on the Server and Services Team. She’s passionate about building great software, and in her spare time, she enjoys reading cheesy novels, watching films, and playing video games.