Mix.install([
{:jason, "~> 1.4"},
{:kino, "~> 0.9", override: true},
{:youtube, github: "brooklinjazz/youtube"},
{:hidden_cell, github: "brooklinjazz/hidden_cell"},
{:smart_animation, github: "brooklinjazz/smart_animation"},
{:visual, github: "brooklinjazz/visual"}
])
Upon completing this lesson, a student should be able to answer the following questions.
- When and why might we use enumeration in programming?
- How do we transform every element in an enumerable?
- How do we remove elements from an enumerable?
- How do we use every element in an enumerable to build an acculumator and return a transformed value?
Enumeration is the act of looping through elements. Unlike iteration, which you may have encountered in other programming languages, enumeration does not allow you to change (mutate) elements in the original collection.
For example, we could enumerate through a list of numbers from one to five, and transform them into a new list of doubled numbers.
flowchart
subgraph Original
direction LR
1 --- 2 --- 3 --- 4 --- 5
end
subgraph Transformed
direction LR
T2[2] --- T4[4] --- T6[6] --- T8[8] --- T10[10]
end
Original --> Transformed
The Elixir Enum module contains functions for using enumeration with enumerable data structures.
For example, there is the Enum.map/2 function for applying a transformation function to each element in an enumerable.
Enum.map([1, 2, 3, 4, 5], fn element -> element * 2 end)
Certain data types in Elixir implement the Enumerable protocol. That means we can enumerate through elements in these enumerable data types.
Most enumerables are collections, but not all of them. For example a range doesn't contain other elements, but it is enumerable.
Enum.map(1..5, fn element -> element * 2 end)
Data types that store other elements are often referred to as collections. For example, maps, lists, keyword lists, and tuples are all collections.
Many collections are enumerable, but not all of them. For example, a tuple is not enumerable.
Enum.map/2, Enum.filter/2, and Enum.reduce/2 are the most common functions for enumeration. By understanding these functions and when to use them, you'll be able to solve most enumeration problems.
- Enum.map/2 returns the same number of elements with a transformation applied to each.
flowchart
subgraph map
direction LR
subgraph input
direction LR
im1((1)) --> im2((2)) --> im3((3))
end
subgraph output
direction LR
om1[2] --> om2[4] --> om3[6]
end
input --"Enum.map(1..3, fn el -> el * 2 end)"--> output
end
Enum.map(1..3, fn el -> el * 2 end)
- Enum.filter/2 removes element(s) from an enumerable without transforming them.
flowchart
subgraph map
direction LR
subgraph input
direction LR
im1((1)) --> im2((2)) --> im3((3))
end
subgraph output
direction LR
om1((1)) --> om2((2))
end
input --"Enum.filter(1..3, fn el -> el <= 2 end)"--> output
end
Enum.filter(1..3, fn el -> el <= 2 end)
- Enum.reduce/2 uses the elements in an enumerable to build an accumulated value and return a transformed data structure.
flowchart
subgraph map
direction LR
subgraph input
direction LR
im1((1)) --> im2((2)) --> im3((3))
end
subgraph output
direction LR
om1[[6]]
end
input --"Enum.reduce(1..3, fn el, acc -> acc + el end)"--> output
end
Enum.reduce(1..3, fn el, acc -> acc + el end)
Often while programming, you run into problems where you need the ability to do something many many times.
For example, let's say you're creating a shopping application. In this application, customers create a shopping list.
We'll represent this shopping cart as a list of item costs in pennies.
shopping_cart = [100, 200, 150, 200]
We can create a function that will calculate the after tax cost of an item called calculate_tax/1
. Lets say that the tax is 5%
so we can multiply our cost by 1.05
.
calculate_tax = fn cost -> cost * 1.05 end
With four items in our shopping list, it's a bit tedius to call the calculate_tax/1
function on each item. We'd need to bind each item to a variable and call the calculate_tax/1
function on each item individually to determine the after tax cost of each item.
[one, two, three, four] = shopping_cart
[calculate_tax.(one), calculate_tax.(two), calculate_tax.(three), calculate_tax.(four)]
This only works when there are exactly four items. If we want to handle when there is one, two, three, or four items our code gets much more verbose. As you can imagine, this solution is not scalable.
shopping_cart = [100, 200, 150]
case shopping_cart do
[] ->
[]
[one] ->
[calculate_tax.(one)]
[one, two] ->
[calculate_tax.(one), calculate_tax.(two)]
[one, two, three] ->
[calculate_tax.(one), calculate_tax.(two), calculate_tax.(three)]
[one, two, three, four] ->
[calculate_tax.(one), calculate_tax.(two), calculate_tax.(three), calculate_tax.(four)]
end
To solve this problem, we need to apply the calculate_tax/1
function on every element in the list. Any time we need to repeat a similar action over and over again we can use enumeration.
Enumeration is the act of looping through a collection and reading its elements for the sake of returning some output.
flowchart LR
Input ---> Enumeration --> Function --> Enumeration
Enumeration --> Output
With enumeration, we can calculate_tax/1
on any number of items with a single call to Enum.map/2.
shopping_cart = [100, 200, 150]
Enum.map(shopping_cart, fn item -> calculate_tax.(item) end)
Elixir provides the Enum to accomplish enumeration. The Enum module contains a large amount of useful functions that all work on collection data types such as lists, ranges, maps, and keyword lists.
Most enumeration problems can be solved with the following Enum functions.
- Enum.map/2 enumerate over every element and create a new collection with a new value.
- Enum.reduce/2 and Enum.reduce/3. Enumerate over every element into an accumulated value.
- Enum.filter/2 filter out elements from a collection.
Enum.map/2 applies the callback function on every element in a collection to build a new list.
Each element in the new list will be the return value of the provided callback function.
Enum.map(1..10, fn element -> element * 2 end)
Enum.filter/2 applies the callback function to every element in the collection and keeps an element when the callback function returns true
.
Enum.filter(1..10, fn element -> rem(element, 2) == 0 end)
Enum.reduce/2 and Enum.reduce/3 applies the callback function to every element in the collection and builds an accumulated value stored between each function call.
When complete, it returns the result of the last function call, which is typically the accumulated value
# 0 Is The Initial Accumulator Value
Enum.reduce(1..3, 0, fn element, accumulator ->
element + accumulator
end)
Enum.reduce/2 uses the first value in the collection as the initial accumulator. Generally (but not always) Enum.reduce/3 is preferable for the sake of clarity, and we will prefer it for most use cases.
Enum.reduce(1..3, 5, fn element, accumulator ->
element + accumulator
end)
Enum.reduce(1..3, fn element, accumulator ->
element + accumulator
end)
The Enum module also provides other useful functions. Here are a handful of the most used.
- Enum.all?/2 check if all elements in a collection match some condition.
- Enum.any?/2 check if any elements in a collection match some condition.
- Enum.count/2 return the number of elements in a collection collection.
- Enum.find/3 return an element in a collection that matches some condition.
- Enum.random/1 return a random element in a collection.
Anytime we're working with a collection, we should refer to the Enum module to see if it has a built-in solution we can use. Alternatively, we can build nearly all functionality using only Enum.filter/2, Enum.reduce/3, and Enum.map/2 so consider developing the most familiarity with these three functions.
Enum.map/2 allows you to enumerate through the collection you provide it as its first argument. It then calls a function that you provide it as its second argument on each element. Finally it returns a new collection with the modified values.
flowchart LR
A[Collection] --> E[Enum.map]
E --> Function
Function --> E
E --> B[New Collection]
Here's an example that doubles all the integers in a list.
Enum.map([1, 2, 3, 4], fn integer -> integer * 2 end)
flowchart LR
A["[1, 2, 3, 4]"] --> E[Enum.map]
E --> F["fn integer -> integer * 2 end"]
F --> E
E --> B["[2, 4, 6, 8]"]
It's useful to be aware that you can use ranges with enumerables to easily enumerate over large ranges without needing to define every element in the list.
Enum.map(1..100, fn integer -> integer * 2 end)
Enum.map/2 accepts any enumerable, and always returns a list, regardless of the input.
keyword lists and maps will be treated as lists of tuples for the sake of enumeration.
Enum.map(%{"one" => 1, "two" => 2}, fn element -> element end)
Enum.map([one: 1, two: 2], fn element -> element end)
We can even pattern match on the key and value in the tuple.
Enum.map(%{"one" => 1, "two" => 2}, fn {_key, value} -> value end)
Enum.map([one: 1, two: 2], fn {key, _value} -> key end)
Use Enum.map/2 to convert a list of integers from 1
to 10
to strings.
# Expected Output
["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]
Example Solution
Enum.map(1..10, fn integer -> "#{integer}" end)
Enter your solution below.
Enum.reduce/2 allows you to enumerate over a collection and build up an accumulated value with each enumeration.
Unlike Enum.map/2 this gives you significantly more control over your output, as the data type of the accumulator and return value do not have to be a list.
flowchart LR
A[Collection] --> E[Enum.reduce]
E -- Accumulator --> Function
Function -- Accumulator --> E
E --Final Accumulator--> B[Output]
For example, we can sum all of the numbers in a list by building up an accumulated sum.
list = [1, 2, 3, 4]
Enum.reduce(list, fn integer, accumulator -> integer + accumulator end)
The function you provide as the second argument of Enum.reduce/2 will be called with the current element, and current accumulator.
The first value in the collection will be the initial accumulator value.
frames = [
"
First, we define the call the [Enum.reduce/2](https://hexdocs.pm/elixir/Enum.html#reduce/2) function with a list, and a function.
```elixir
Enum.reduce([1, 2, 3, 4], fn integer, accumulator -> integer + accumulator end)
```
",
"
The first element in the list `1` is the initial accumulator value.
```elixir
Enum.reduce([2, 3, 4], fn integer, 1 -> integer + 1 end)
```
",
"
The function is called on the next element `2`. The next accumulator is 2 + 1
```elixir
Enum.reduce([3, 4], fn 2, 1 -> 2 + 1 end)
```
",
"
The function is called on the next element `3`. The next accumulator is 3 + 3
```elixir
Enum.reduce([4], fn 3, 3 -> 3 + 3 end)
```
",
"
The function is called on the next element `4`. The next accumulator is 4 + 6
```elixir
Enum.reduce([], fn 4, 6 -> 4 + 6 end)
```
",
"
4 + 6 equals 10.
```elixir
Enum.reduce([], fn 4, 6 -> 10 end)
```
",
"
`10` is the last accumulator value, so [Enum.reduce/2](https://hexdocs.pm/elixir/Enum.html#reduce/2) returns `10`.
```elixir
10
```
"
]
SmartAnimation.new(0..(Enum.count(frames) - 1), fn i ->
Kino.Markdown.new(Enum.at(frames, i))
end)
Alternatively, we can provide a default value for the accumulator with Enum.reduce/3.
Enum.reduce/3 will call the function on every element, rather than setting the initial accumulator as the first element. Below, we sum all of the numbers in the list, with an initial sum of 10
.
Enum.reduce([1, 2, 3], 10, fn integer, accumulator -> integer + accumulator end)
Use Enum.reduce/3 or Enum.reduce/2 to sum all of the even numbers from 1
to 10
. It should return 30
.
2 + 4 + 6 + 8 + 10 = 30
Example solution
Enum.reduce(1..10, 0, fn int, acc ->
if rem(int, 2) == 0 do
acc + int
else
acc
end
end)
Enter your solution below.
The Enum.filter/2 function allows us to filter elements in a collection. Enum.filter/2 calls the function with each element in the collection. If the function returns false
then the element is filtered out.
flowchart LR
C[Collection] --> E[Enum.filter]
E --> F[Function]
F -- boolean --> E
F --> true --> A[Keep]
F --> false --> B[Remove]
E --> O[Filtered Collection]
Enum.filter([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], fn integer -> integer <= 5 end)
use Enum.filter/2 to create a list of odd numbers from 1
to 10
and a list of even numbers from 1
to 10
.
Example Solution
even_numbers = Enum.filter(1..10, fn integer -> rem(integer, 2) == 0 end)
odd_numbers = Enum.filter(1..10, fn integer -> rem(integer, 2) != 0 end)
Enter your solution below.
Enum.all?/2 checks that all elements in a collection match some condition.
Enum.all?/2 executes the callback function provided on each element. If every element returns truthy, then Enum.all?/2 returns true
.
flowchart LR
Collection --> E[Enum.all?/2]
E --> Function
Function -- truthy --> E
E --> Boolean
Enum.all?([1, 2, 3], fn integer -> is_integer(integer) end)
If a single element returns a falsy value, then Enum.all?/2 returns false
.
Enum.all?([1, 2, 3, "4"], fn element -> is_integer(element) end)
For performance reasons, Enum.all?/2 will complete as soon as it finds a single element that returns a falsey value when called with the function.
Notice the code below should finish very quickly because the very first element fails the condition.
Enum.all?(1..10_000_000, fn integer -> is_bitstring(integer) end)
If Enum.all?/2 must traverse the entire collection if all elements pass the condition, or if a failing element is towards the end of the list.
Notice the code below should take awhile to finish running because every element passes the condition.
Enum.all?(1..10_000_000, fn integer -> is_integer(integer) end)
Use Enum.all?/2 to determine if all of the colors
in this list of colors are :green
. You may change the value of colors
to experiment with Enum.all?/2.
colors = [:green, :green, :red]
Enum.any?/2 checks if any elements in a collection pass some condition. If a single element returns a truthy value when called with the callback function, then Enum.any?/2 returns true
.
flowchart LR
Collection --> E[Enum.any?/2]
E --> Function
Function -- truthy --> E
E --> Boolean
Enum.any?([1, "2", "3"], fn element -> is_integer(element) end)
Enum.any?/2 returns as soon as an element in the collection returns a truthy value.
Notice the code below finishes very quickly, because the first element in the collection passes the condition.
Enum.any?(1..10_000_000, fn integer -> is_integer(integer) end)
However, the following takes awhile to run because all elements in the collection fail the condition.
Enum.any?(1..10_000_000, fn integer -> is_bitstring(integer) end)
Use Enum.any?/2 to determine if any of the animals in the animals
list are :dogs
. You may change the animals
list to experiment with Enum.any?/2.
animals = [:cats, :dogs, :bears, :lions, :penguins]
Enum.count/1 returns the number of items in a collection.
flowchart LR
Collection --> Enum.count --> Integer
Enum.count([1, 2, 3])
The type of elements does not matter. Collections with multiple elements still only count as a single individual element.
Enum.count([{}, "hello", %{}, [[[]]]])
In the Elixir cell below, count the number of elements in the collection
. It should return 5
.
collection = [1, 2, 3, 4, 5]
Enum.find/3 takes in a collection and a function. Then searches the collection and returns the first element that returns true when called as the argument to the passed-in function.
Enum.find(["hello", 2, 10], fn each -> is_integer(each) end)
If no element is found, Enum.find/2 returns nil
.
Enum.find(["h", "e", "l", "l", "o"], fn each -> is_integer(each) end)
You might notice the arity of the function is 3
even though we passed in only 2
arguments. That's because there's an optional default
value argument. The default
value will be returned instead of nil
if no element is found.
Enum.find(["h", "e", "l", "l", "o"], 10, fn each -> is_integer(each) end)
Use Enum.find/2 to find the first even integer in this list.
[1, "2", "three", 4, "five", 6]
Enum.random/1 returns a random element in a collection. It's often used to generate random numbers in a range.
Enum.random(1..10)
Generate a list with ten random integers from 0
to 9
.
Example solution
Enum.map(1..10, fn _ -> Enum.random(0..9) end)
Enter your solution below.
When we provide a function to any of the Enum we're passing an anonymous function, which will be called under the hood.
We'll bind this function to a variable and inspect every value to demonstrate.
my_function = fn element -> IO.inspect(element) end
Enum.map(1..10, my_function)
We can use the capture operator &
syntax to provide module functions.
defmodule NonAnonymous do
def function(element) do
IO.inspect(element)
end
end
Enum.map(1..10, &NonAnonymous.function/1)
This can be used with any of Elixir's built-in functions as well, as long as their arity matches the callback function.
Enum.map(1..10, &is_integer/1)
We can also use the capture operator alone to create an anonymous function. However, we generally don't recommend this as it typically reduces readability. &1
is the first argument to the function.
Enum.map(1..10, &(&1 * 2))
The Enum module has many more functions. You'll have the opportunity to encounter more as you need them to solve future challenges.
For more information, you may also wish to read:
We have several exercises dedicated to the Enum module as part of this curriculum. However, if you wish for more practice Exercism.io has a number of excellent exercises.
DockYard Academy now recommends you use the latest Release rather than forking or cloning our repository.
Run git status
to ensure there are no undesirable changes.
Then run the following in your command line from the curriculum
folder to commit your progress.
$ git add .
$ git commit -m "finish Enum reading"
$ git push
We're proud to offer our open-source curriculum free of charge for anyone to learn from at their own pace.
We also offer a paid course where you can learn from an instructor alongside a cohort of your peers. We will accept applications for the June-August 2023 cohort soon.