Monday, January 30, 2017

Elixir - Enumerables

An enumerable is an object that may be enumerated. "Enumerated" means to count off the members of a set/collection/category one by one (usually in order, usually by name).
Elixir provides the concept of enumerables and the Enum module to work with them. The functions in the Enum module are limited to, as the name says, enumerating values in data structures.
Example of an enumerable data structure is a list, tuple, map, etc. The Enum module provides us with a little over 100 functions to deal with enums. We'll have a look at some of them here. All of these functions take an enumerable as first element and a function as the second and work on them. Their descriptions are described below.

all?

When we use all? the entire collection must evaluate to true otherwise false will be returned. For example if we want to check if all of the elements in the list are odd numbers, then:
res = Enum.all?([1, 2, 3, 4], fn(s) -> rem(s,2) == 1 end)
IO.puts(res)
When running above program, it produces following result:
false
This is because not all elements of this list are odd.

any?

As the name suggests, this function returns true if any element of the collection evaluates to true. For example:
res = Enum.any?([1, 2, 3, 4], fn(s) -> rem(s,2) == 1 end)
IO.puts(res)
When running above program, it produces following result:
true

chunk

This function divides our collection in small chunks of size provided as the second argument. For example:
res = Enum.chunk([1, 2, 3, 4, 5, 6], 2)
IO.puts(res)
When running above program, it produces following result:
[[1, 2], [3, 4], [5, 6]]

each

It may be necessary to iterate over a collection without producing a new value, for this case we use each:
Enum.each(["Hello", "Every", "one"], fn(s) -> IO.puts(s) end)
When running above program, it produces following result:
Hello
Every
one

map

To apply our function to each item and produce a new collection we use the map function. It is one of the most useful constructs in functional programming as it is quite expressive and short. Let us take an example. Let us double the values stored in a list and store it in a new list res:
res = Enum.map([2, 5, 3, 6], fn(a) -> a*2 end)
IO.puts(res)
When running above program, it produces following result:
[4, 10, 6, 12]

reduce

Reduce helps us reduce our enumerable to a single value. To do this we supply an optional accumulator (5 in this example) to be passed into our function; if no accumulator is provided the first value is used:
res = Enum.reduce([1, 2, 3, 4], 5, fn(x, accum) -> x + accum end)
IO.puts(res)
When running above program, it produces following result:
15
The accumulator is the initial value passed to the fn. From the second call onwards the value returned from previous call is passed as accum. We can also use reduce without the accumulator:
res = Enum.reduce([1, 2, 3, 4], fn(x, accum) -> x + accum end)
IO.puts(res)
When running above program, it produces following result:
10

uniq

uniq removes duplicates from our collection and returns only the set of elements in the collection. For example:
res = Enum.uniq([1, 2, 2, 3, 3, 3, 4, 4, 4, 4])
IO.puts(res)
When running above program, it produces following result:
[1, 2, 3, 4]

Eager Evaluation

All the functions in the Enum module are eager. Many functions expect an enumerable and return a list back. This means that when performing multiple operations with Enum, each operation is going to generate an intermediate list until we reach the result. For Example,
odd? = &(rem(&1, 2) != 0)
res = 1..100_000 |> Enum.map(&(&1 * 3)) |> Enum.filter(odd?) |> Enum.sum
IO.puts(res)
When running above program, it produces following result:
7500000000
The example above has a pipeline of operations. We start with a range and then multiply each element in the range by 3. This first operation will now create and return a list with 100_000 items. Then we keep all odd elements from the list, generating a new list, now with 50_000 items, and then we sum all entries.
The |> symbol used in the snippet above is the pipe operator: it simply takes the output from the expression on its left side and passes it as the first argument to the function call on its right side. It’s similar to the Unix | operator. Its purpose is to highlight the flow of data being transformed by a series of functions. Without the pipe operator the code looks quite complicated:
Enum.sum(Enum.filter(Enum.map(1..100_000, &(&1 * 3)), odd?))
There are a LOT more of functions and the one's listed here barely scratch the surface of what the enum module has to offer. Check out the documentation to get a better sense of what other functions are available for use.

No comments:

Post a Comment