Skip to content

Latest commit

 

History

History
806 lines (573 loc) · 27.2 KB

enum.livemd

File metadata and controls

806 lines (573 loc) · 27.2 KB

Enum

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"}
])

Navigation

Review Questions

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?

Overview

Enumeration

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
Loading

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)

Enumerable

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)

Collections

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.

Map, Filter, And Reduce

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
Loading
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
Loading
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
Loading
Enum.reduce(1..3, fn el, acc -> acc + el end)

Scenario

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
Loading

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)

The Enum Module

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 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)

Other Handy Functions

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

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]
Loading

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]"]
Loading

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)

Your Turn

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 And Enum.reduce/3

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]
Loading

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)

Enum.reduce/3

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)

Your Turn

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.

Enum.filter

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]
Loading
Enum.filter([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], fn integer -> integer <= 5 end)

Your Turn

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

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
Loading
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)

Your Turn

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

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
Loading
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)

Your Turn

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

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

flowchart LR
  Collection --> Enum.count --> Integer
Loading
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", %{}, [[[]]]])

Your Turn

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

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)

Your Turn

Use Enum.find/2 to find the first even integer in this list.

[1, "2", "three", 4, "five", 6]

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

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.

Capture Operator And Module Functions

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))

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:

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.

Commit Your Progress

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.

Navigation