Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Enum

enum.livemd

Enum

Mix.install([
  {:kino, github: "livebook-dev/kino", override: true},
  {:kino_lab, "~> 0.1.0-dev", github: "jonatanklosko/kino_lab"},
  {:vega_lite, "~> 0.1.4"},
  {:kino_vega_lite, "~> 0.1.1"},
  {:benchee, "~> 0.1"},
  {:ecto, "~> 3.7"},
  {:math, "~> 0.7.0"},
  {:faker, "~> 0.17.0"},
  {:utils, path: "#{__DIR__}/../utils"}
])

Navigation

Return Home Report An Issue

Setup

Ensure you type the ea keyboard shortcut to evaluate all Elixir cells before starting. Alternatively you can evaluate the Elixir cells as you read.

Overview

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.

You’re responsible for displaying the cost of each item to the user after taxes.

Given a list of costs in pennies, how might you do that with what you’ve learned so far?

[100, 200, 150, 200]

With only four items, That would still be a fairly difficult task. You would have to extract out each value with pattern matching, and then apply some calculate_tax/1 function to each value, then rebuild a new list with the new values

[one, two, three, four] = [100, 200, 150, 200]

[calculate_tax.(one), calculate_tax.(two), calculate_tax.(three), calculate_tax.(four)]

However the above would only work for four items, you would need to create different cases for every size of list you want to support.

list = [100, 200, 150, 200]

case list 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

Now imagine if you had to support a hundred items. Or if you have enterprise customers who buy thousands of items! This simply doesn’t scale.

To handle scenarios where we need to repeat an 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 line.

Enum.map(list, fn item -> calculate_tax(item) end)

This lesson will cover tools and techniques to accomplish enumeration. In total we will cover:

  • Enumerable Collections that support enumeration.
  • How to use the Enum module’s built-in functionality with Collections.

First, it’s useful to know what we can enumerate on.

Enum

We use the Enum module to accomplish enumeration. The Enum module contains a large amount of useful functions that all work on enumerable data types.

In Elixir, certain data types are enumerable which means that we have the ability to enumerate through them and apply some function on each element.

These structures are called Collections because they contain a collection of elements. You are already familiar with them. They are lists, keyword lists, ranges, and maps.

We’ll focus on common Enum functions you’ll use frequently.

  • 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.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/1 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.

The Enum module has a lot more functionality. Whenever you think to yourself “How can I __ in a collection” you should see if the Enum module has a built-in solution. It’s not realistic to learn it all upfront. Instead you’ll learn functions over time whenever you run into a problem that requires Enum as a solution.

Enum.map

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. It then 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)

Your Turn

Back to our example above, how would you use Enum.map/2 to convert [1, 2] into a keyword list? Remember that keyword lists are actually lists of tuples.

In the Elixir cell below, convert [1, 2] into [{:int, 1}, {:int, 2}]

Your Turn (Bonus)

Bonus: In the Elixir cell below, instead of using the generic :int atom key, convert 1..9 into [{:one, 1}, {:two, 2}, ...and so on]. You only need to handle numbers from 0 to 9.

Enum.reduce

Enum.reduce/2 allows you to enumerator over a collection, except you have more flexibility over the output.

You can build up a value using an accumulator, and then return a final value.

flowchart LR
  A[Collection] --> E[Enum.reduce]
  E -- Accumulator --> Function
  Function -- Accumulator --> E
  E --> B[Output]

Notice that the output could be any data type, not just a collection. For example, you could use this to take in a list of numbers and sum all of the numbers together.

list = [1, 2, 3, 4]

Enum.reduce(list, fn integer, accumulator -> integer + accumulator end)

By default, the first value in the collection will be the initial accumulator value. The function that you pass in as the second argument will then be called on every element after the first.

Enum.reduce/3

You can also override the default accumulator with Enum.reduce/3. Enum.reduce/3 takes three arguments. The first is the collection, the second is the default accumulator, and the third is the function to call with each element and the accumulator.

Enum.reduce/3 will call the function on every element, rather than setting the initial accumulator as the first element.

Enum.reduce([1, 2, 3], 10, fn integer, accumulator -> integer + accumulator end)

Let’s break that down step by step.

Utils.slide(:reduce)

Enum.filter

The Enum.filter/2 function allows us to filter elements in a collection. Enum.filter/2 takes in two arguments. The first is a collection, and the second is a function to call on 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)

Enum.all?/2

Enum.all?/2 takes in a collection and a function. Enum.all?/2 calls the function on every element in the collection. If any element returns false then Enum.all?/2 returns false. Otherwise, Enum.all?/2 returns true.

flowchart LR
Collection --> E[Enum.all?]
E --> Function
Function -- boolean --> E
E --> Boolean
Enum.all?([1, 2, 3], fn integer -> integer < 5 end)
Enum.all?([1, 2, 10], fn integer -> integer < 5 end)

Enum.any?/2

Enum.any?/2 Is much like Enum.all/2 except it checks if any element in the collection returns true when called with the passed in function.

Enum.any?([1, 2, 3], fn integer -> integer === 1 end)
Enum.any?([2, 2, 3], fn integer -> integer === 1 end)

Enum.count/1

Enum.count/1 returns the number of items in a collection.

flowchart LR
  Collection --> Enum.count --> Integer
Enum.count([1, 2, 3])

Your Turn

In the Elixir cell below, count the number of elements in the collection

collection = [1, 2, 3, 4, 5]

Enum.find/2

Enum.find/2 takes in a collection and a function. Enum.find/2 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)

Your Turn

In the Elixir cell below, use Enum.find/2 to find 5 in this list.

list = [1, 2, 3, 4, 5]

Enum.random/1

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)

Your Turn

In the Elixir cell below, use Enum.random/1 to retrieve a random element from the collection.

collection = ["one", 3, {}]

Enum On Other Collections

lists are only one of many collection types.

Keyword Lists

Remember that keyword lists are simply lists of tuples.

[{:one, 1}, {:two, 2}]

So whenever an Enum module function uses an element inside of the collection, it’s a tuple.

Enum.reduce([one: 1], fn tuple, _accumulator -> tuple end)

Maps

Internally, maps are also treated as tuples in a collection.

Enum.reduce(%{key: "value"}, fn tuple, _accumulator -> tuple end)

Your Turn

In the Elixir cell below, use Enum.reduce/2 to sum all of the values in a map. So %{key1: 1, key2: 10} should become 11.

map = %{key1: 1, key2: 10}

In the Elixir cell below, use Enum.reduce/2 to sum all of the keys and values in a map. So %{1 => 15, 10 => 12} should become 38 (1 + 15 + 10 + 12) .

map = %{1 => 15, 10 => 12}

Use Enum.reduce/2 to sum all of the values in a keyword list. so [add: 20, add: 5] should become 25.

keyword_list = [add: 20, add: 5]

Further Reading

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:

Commit Your Progress

Run the following in your command line from the project folder to track and save your progress in a Git commit.

$ git add .
$ git commit -m "finish enum section"