How to repeat something many times in Elixir?

How to write a loop in Elixir which iterates N times?

Kamil Lelonek
Kamil Lelonek  - Software Engineer

--

A loop is something that lets you repeat an action many times. The purpose of a loop is to iterate over all elements from a given range.

Enumerable

More often than not, you want to leverage Enum module whose functions work on any data type that implements the Enumerable protocol. When you invoke a function in the Enum module, the first argument is usually a collection that must implement this protocol.

For example, the expression:

iex(1)> Enum.map([1, 2, 3], &IO.inspect/1)
1
2
3
[1, 2, 3]

builds a mapped list by calling the mapping function &IO.inspect/1 (which fortunately returns the printed element by the way) on every element in the given collection.

However, if you want to repeat it for a list of 100 elements (i.e. 100 times) it will be very tedious to write [1, 2, 3, 4, ..., 100]. For such cases, we should utilize the Range module which represents a sequence of consecutive integers:

iex(1)> result = Enum.each(0..3, &IO.inspect/1)
0
1
2
3
:ok
iex(2)> result
:ok

Moreover, we use each/2 function to produce side effects — to invoke the given function N times.

For comprehensions

Comprehensions are syntactic sugar for such loops over an Enumerable — they group those common tasks into the for special form.

iex(1)> result = for i <- 0..3, do: i
[0, 1, 2, 3]
iex(2)> result
[0, 1, 2, 3]

Comprehensions generally provide a much more concise representation than using the equivalent functions from the Enum and Stream modules. In our case though, the difference is subtle.

Tips and tricks

It’s worth mentioning how exactly ranges are constructed. Sometimes, you may want to start your range from 0 and provide an upper boundary as a parameter. In some cases though, you don’t want to iterate at all. For example: “I want to adjust an upper boundary and skip looping in special cases”. If you think it’s simple, have a look:

iex(1)> upper = 0
0
iex(2)> range = 0..upper
0..0

We have just created a range from 0 to… 0, which could make us think there’s no range at all or, in other words, the range is “empty”. Let’s see how many elements it has or, even better, what is the underlying list generated:

iex(3)> Enum.count(range)
1
iex(4)> Enum.to_list(range)
[0]

It turns out, that the list is not empty. Indeed, it has exactly one element which means the iteration will happen! Let’s see the following:

iex(1)> for i <- 0..0, do: i
[0]

It’s pretty understandable now why it happened. We might have expected to produce an empty range when the lower boundary equals to the upper one, but in reality, since we know the created list under the hood, we are aware that the loop will run at least once.

Subscribe to get the latest content immediately
https://tinyletter.com/KamilLelonek

Summary

To sum up, pay attention to how you construct your ranges and what you expect from your loops. If you really want to skip iteration, you are able to use guards there:

iex(1)> for i <- 0..10, i > 0, do: IO.inspect(i)
1
2
3
4
5
6
7
8
9
10
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

This way, you won’t only prevent from looping over an “empty” range:

iex(2)> for i <- 0..0, i > 0, do: IO.inspect(i)
[]

but also you will iterate exactly as many times as the upper boundary is :)

For side effect, you can also leverage pattern matching and each/2 function from the Enum module:

iex(1)> Enum.each(0..10, fn
...(1)> 0 -> :noop
...(1)> i -> IO.inspect(i)
...(1)> end)
1
2
3
4
5
6
7
8
9
10
:ok

I hope you enjoyed my article and let me know whether you know some other tricks regarding ranges, enums and looping.

--

--