Learn With Me: Elixir - Enumerables (#16)
An enumerable in Elixir is anything that can be iterated over. This is the equivalent to a class that implements IEnumerable<T>
in C#. I think the closest thing in Javascript is the iterator protocol, which allows you to create data that can be iterated over.
Elixir defines an enumerable as any data structure that implements the Enumerable
protocol. I haven't talked about protocols yet, and that's because I haven't yet learned anything about them other than that they exist. I've gotten the impression from the few mentions I've seen so far that protocols are the equivalent of interfaces in Elixir. That sounds interesting, and I look forward to learning more about protocols.
All we need to know for now is that there are certain data types that can be iterated over. Common enumerables are collections such as lists and maps and streams. I have not yet learned about streams, so I'm not sure what those involve.
Any enumerable can be operated on with the functions in the Enum
module. All of the functions in the Enum
module are O(n) operations, since they know nothing more about the data they are operating on other than it's an enumerable, and can only perform calculations by iterating over the available data..
Many of the functions in the Enum
module are higher-order functions, where we pass the data to be operated on and a function that describes how to operate on that data. This is a common pattern in functional languages, and is typically used in place of loops from imperative languages.
We just specify a function (which may or may not be reusable) and it will be applied to the elements in the enumerable. Javascript has some of these higher-order functions built into arrays and other higher-order functions are provided by libraries like lodash. C# has LINQ, which provides some useful higher-order extensions to classes that implement the IEnumerable
interface.
The in
operator in Elixir (which is probably actually implemented as a function somewhere) will determine if an element is present in the enumerable. This is the equivalent to IEnumerable<T>.Contains()
in C# or array.includes()
in Javascript (ES2016+).
C# Example:
List<int> numberList = new List<int>() { 1, 3, 5, 6, 7};
numberList.Contains(3); //Evaluates to true
numberList.Contains(2); //Evaluates to false
Javascript Example:
var numbers = [1, 3, 5, 6, 7];
numbers.includes(3); //Evaluates to true
numbers.includes(2); //Evaluates to false
Elixir Example:
iex> numbers = [1, 3, 5, 6, 7]
[1, 3, 5, 6, 7]
iex> 3 in numbers
true
iex> 2 in numbers
false
The functions in the Enum module represent a typical functional programming way of thinking. The higher order functions transform the data using another function. You declare what you want to do to the collection and it happens within the function. The equivalents to these functions can be found in other non-functional languages as well, particularly C# and Javascript. This is an example of functional concepts leaking into imperative languages. The fact that many functional concepts have been popping up in non-functional languages over the past decade or more means that I had already encountered some functional concepts in other languages. So far, this has made it easier for me to adjust to Elixir.
Useful Enum Functions
I'll cover a few useful Enum
functions briefly. I'll cover all the Enum
functions in detail in a future topic.
Mapping
Enum.map/2
transforms every element in an enumerable. The function that we pass to it determines how each element will be transformed.
iex> numbers = [1, 2, 3, 4, 5]
[1, 2, 3, 4, 5]
iex> doubled_numbers = Enum.map(numbers, fn x -> x * 2 end)
[2, 4, 6, 8, 10]
iex> squared_numbers = Enum.map(numbers, fn x -> x * x end)
[1, 4, 9, 16, 25]
I used Enum.map/2
and an anonymous function to double all the numbers in a collection and then I passed a different function to square the numbers.
I don't have to specify an anonymous function every time. I can also use named functions or reusable anonymous functions.
iex> double = &(&1 * 2)
#Function<6.127694169/1 in :erl_eval.expr/5>
iex> square = &(&1 * &1)
#Function<6.127694169/1 in :erl_eval.expr/5>
iex> Enum.map(numbers, double)
[2, 4, 6, 8, 10]
iex> Enum.map(numbers, square)
[1, 4, 9, 16, 25]
Notice that I used the shortcut syntax to create the double
and square
functions.
I can also use Enum.map/2
function and combined it with the double
and square
functions I created earlier to create reusable functions that will double or square collections.
iex> double = &(Enum.map(&1, double))
#Function<6.127694169/1 in :erl_eval.expr/5>
iex> double.(numbers)
[2, 4, 6, 8, 10]
iex> square = &(Enum.map(&1, square))
#Function<6.127694169/1 in :erl_eval.expr/5>
iex> square.(numbers)
[1, 4, 9, 16, 25]
Filtering
Another useful function is Enum.filter/2
, which filters out certain elements from a collection. The function that I pass to it determines which elements will be kept and which will be discarded. When the function returns true, the corresponding element will be kept; otherwise, it will be discarded.
iex> is_even = fn number -> rem(number, 2) == 0 end
#Function<6.127694169/1 in :erl_eval.expr/5>
iex> is_even.(2)
true
iex> is_even.(7)
false
iex> is_odd = fn number -> rem(number, 2) == 1 end
#Function<6.127694169/1 in :erl_eval.expr/5>
iex> is_odd.(2)
false
iex> is_odd.(7)
true
iex> Enum.filter(numbers, is_even)
[2, 4]
iex> Enum.filter(numbers, is_odd)
[1, 3, 5]
First, I created two reusable functions, is_even
and is_odd
. Then I tested them to verify that they did what I intended. I used is_even
to filter out the even numbers and keep them. Then I used is_odd
to filter out the odd numbers and keep them.
Concatenating
Enum.concat/2
concatenates two enumerables into a list.
ex> list = [1, "Bob", :ok, 3.14]
[1, "Bob", :ok, 3.14]
iex> Enum.concat(list, [4, 8, 15])
[1, "Bob", :ok, 3.14, 4, 8, 15]
iex> Enum.concat(list, %{name: "Dib", age: 10})
[1, "Bob", :ok, 3.14, {:age, 10}, {:name, "Dib"}]
Maps are enumerable as well, and when we iterate over a map, we get tuples of name-value pairs. So I was able to concat a map with a list and get a list containing both the elements from the initial list and the name-value pairs from the map.