Comparing iteration in JavaScript and Elixir

In JavaScript (and languages like it), it is common to see code like this:

// JavaScript

let array = [-1, 2, -3, 4]
let sum = 0

for (let i = 0; i < array.length; i++) {
if (array[i] < 0)
continue

sum = sum + array[i]
}

assert(sum == 6)

This code is doing a pretty simple task. It calculates the sum of all elements in a which are positive. However, it’s difficult to read at a glance, and this style of iteration is error prone.

An idiomatic translation of this code to Elixir would be:

# Elixir

sum =
[-1, 2, -3, 4]
|> Enum.reject(fn elem -> elem < 0 end)
|> Enum.sum()

assert sum == 6

The Elixir version is more declarative, and certainly easier to read. (To be fair, JavaScript now includes functions like Array.prototype.filter and Array.prototype.reduce that allow you to use a similar style.)

However, a new user might initially try something familiar from other languages and run into some problems:

# Elixir

sum = 0

Enum.each [-1, 2, -3, 4], fn elem ->
unless elem < 0 do
sum = sum + elem
end
end

assert sum == 6 # Failure: sum == 0

Why doesn’t this work?

In languages like JavaScript, variables which are captured by a closure (or from enclosing scopes, like in the first example above) are captured by reference:

// JavaScript

let a = 1

let get = () => a
let set = (value) => a = value

set(2)

assert(get() == 2)

However, in Elixir, variables are captured by value. When we refer to sum in the anonymous function we are passing to Enum.each/2, we are referring to the value that sum had at the time we created the function:

# Elixir

a = 1

get = fn -> a end
set = fn value -> a = value end

set.(2)

assert get.() == 1

a = 2

assert get.() == 1

Additionally, in Elixir, even when we “reassign” a variable, we are not actually changing the original variable. Code like this:

# Elixir

a = 1
a = 2

assert a == 2

is actually equivalent to the following code:

% Erlang

A1 = 1,
A2 = 2,
assert(A2 =:= 2).

So, in our example before, set is not actually assigning to the original a. It’s assigning to a “new” a, and the original a has not changed.

To each their own…

Given everything we just discussed, this code may be somewhat shocking:

# Elixir

use MutableEach

sum = 0

each elem <- [-1, 2, -3, 4], mutable: {sum} do
if elem < 0,
do: continue

sum = sum + elem
end

assert sum == 6

“How is this possible in Elixir? I thought we couldn’t refer to variables by reference!”, you might say. Well, thanks to the power of macros, almost anything is possible in Elixir.

Introducing: MutableEach, a library which implements “mutable” iteration in Elixir.

Ultimately, the example above expands to something that looks somewhat like:

sum = 0

{sum} =
Enum.reduce_while [-1, 2, -3, 4], {sum}, fn elem, {sum} ->
try do
if elem < 0,
do: throw {:mutable_each_continue, {sum}}

sum = sum + elem
{:cont, {sum}}
catch
{:mutable_each_continue, mutable} -> {:cont, mutable}
{:mutable_each_break, mutable} -> {:halt, mutable}
end
end

assert sum == 6

MutableEach mostly relies on Enum.reduce_while/3 and throw. Under the hood, there is still no actual mutability. The variables that are declared as “mutable” are simply provided as an accumulator within reduce_while, automatically returned at the end of each iteration, and then exported back into the original vars after the reduce is complete.

continue and break are implemented as macros which throw a tuple containing an atom representing the type of interrupt (continue or break) and the “mutable” variables. The function generated and passed to reduce_while contains a catch clause that returns {:cont, values} or {:halt, values} depending on the type of interrupt that was thrown.

Just another great example of how powerful and flexible Elixir is, thanks to macros.

Don’t try this at home

I probably shouldn’t have to say this, but this was simply an experiment, and MutableEach should not be used in your Elixir code. Simply using Enum.reduce_while/3 (no throw needed, probably!) directly in your own code is more explicit, and explicit is better than implicit.