Blog Post

Elixir Idioms for JavaScript Developers

Oghenerukevwe Henrietta Kofi
Illustration: 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 ifs 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.

Author
Oghenerukevwe Henrietta Kofi Server and Services Engineer

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.

Share post
Free trial Ready to get started?
Free trial