September 19, 2020

The Elixir Enum Module

To help myself in learning more Elixir, I'm going to continue these Elixir guide/tutorial posts where I try to explain a different piece of Elixir in each post. Today's post will be covering the Enum module and it's usage.

The Enum Module provides a set of algorithms that enumerate over collections according to the Enumerable protocol: iex> Enum.map([1, 2, 3], fn(x) -> x * 2 end) [2,4,6]

Some particular types, like dictionaries, yield a specific format on enumeration. For dicts, the argument is always a {key, value} tuple:

iex> dict = %{a: 1, b: 2}
iex> Enum.map(dict, fn {k, v} -> {k, v * 2} end)
[a: 2, b: 4]

Below are some other functions available in the Enum Module along with a link to it's definition in the Enum Module documentation.

The Capture Operator

Let’s first talk about capturing function. Capture means "&" can turn a function into an anonymous function which can be passed as arguments to other function or be bound to a variable.

& can capture two types of functions, a function with given name and arity from a module.

The notation is: &(module_name.function_name/arity) ex: speak = &(IO.puts/1) speak.("hello") # hello

We capture puts function from IO module and bind it with a local name speak.

The capture operator can be a little difficult to wrap your head around, so here are some examples and a helpful image to help grasp this concept:

image.png

# Multiple each number by itself
Enum.map [1, 2, 3], fn(num) ->
  num * num
end

# Shortened with capture operator:
# Parentheses are required around the capture in this
# case to make it clear where the capture starts and ends.
Enum.map([1, 2, 3], &(&1 * &1))

When you are capturing a named function, you don’t need the parentheses:

# Remove \n chars from the end of each word
Enum.map ["hello\n", "there\n"], fn(word) ->
  String.replace(word, "\n", "")
end

# Shortened with capture operator:
Enum.map(["hello\n", "there\n"], 
         &String.replace(&1, "\n", ""))

Read the documentation on the Capture operator for more details.

Stream

Stream is a lazy version of the Enumerable module. Note that the functions in the Enum module are eager: they always start the enumeration of the given collection. The Stream module allows lazy enumeration of collections and provides infinite streams. It implements most Enum functions, but instead of returning a modified list, it returns a struct like this:

%Stream{
  enum: [...], # Enumerable to iterate through
  funs: [...]  # Anonymous functions to run
}

Since the majority of the functions in Enum enumerate the whole collection and return a list as result, infinite streams need to be carefully used with such functions, as they can potentially run forever. For example:

Enum.each Stream.cycle([1,2,3]), &IO.puts(&1)

Streams are lazy, and only iterate over the list once:

# Iterates over the list twice
list
|> Enum.filter(&is_number/1)
|> Enum.filter(&(&1 * 2 == 4))
# Iterates over the list once
list
|> Stream.filter(&is_number/1)
|> Stream.filter(&(&1 * 2 == 4))
|> Enum.into([])

Use Enum.into/2 or Stream.run/1 to make a stream do work.

list
|> Stream.filter(&is_number/1)
|> Stream.filter(&(&1 * 2 == 4))
|> Enum.into([])

[1, 2, 3]
|> Stream.each(&IO.puts/1)
|> Stream.run

Checkout some more Stream building functions in the documentation: - cycle/1 - iterate/2 - resource/3

Resources

tags: programming functions enum elixir code