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"}
])
- When should we use
map
vsfilter
vsreduce
? - How can we build an accumulated value using reduce?
Reduce is an incredibly powerful tool you can leverage in a wide variety of circumstances.
Reduce (sometimes called fold) is a basic building block in functional programming. Almost all of the functions in the Enum module can be implemented on top of reduce. Those functions often rely on other operations, such as Enum.reverse/1, which are optimized by the runtime.
If you'd like to learn more about reduce, there's an excellent video by Paul Fioravanti.
YouTube.new("https://www.youtube.com/watch?v=OQrfedclHfk")
As we've seen, other Enum functions such as Enum.map/2 or Enum.filter/2 take in a collection as input and produce a list as output.
flowchart LR
C1[Collection] --> Enum.map/2 --> L1[List]
C2[Collection] --> Enum.filter/2 --> L2[List]
Reduce allows you to take any collection and produce any output. That's powerful.
flowchart LR
C1[Collection] --> Enum.reduce/3 --> ANYTHING!!!
Anytime you have a collection, and want to produce a non-list output, Enum.reduce/3 might be the right tool for the job.
Enum.reduce/3 works by enumerating over the collection and building up an accumulator. Instead of applying a change to each element in the collection, we transform the accumulator by applying a callback function on every element.
For example, we can simply return 0
in the body of the callback function to return this value as the accumulator.
Enum.reduce(1..3, 0, fn _integer, _acc -> 0 end)
With each step in Enum.reduce/3 we create a new acculumator of 0
.
flowchart TB
subgraph Accumulator
direction TB
A1 --> A2 --> A3
end
subgraph Element
direction TB
1 --> 2 --> 3
end
A1[0]
A2[0]
A3[0]
Let's look at a more practical example of taking a collection, and returning a non-list output.
In the Anagram exercise you previously completed, you built an Anagram.anagram?/2
function to determine if two strings are anagrams. One solution to the problem is to build a map storing the character counts of two strings.
For example, to determine if "state"
and "taste"
are anagrams of each other, we can convert them into the following maps with a count of each character, and check if they are equal. Remember that order in maps does not matter.
%{"s" => 1, "t" => 2, "a" => 1, "e" => 1} == %{"t" => 2, "a" => 1, "s" => 1, "e" => 1}
We can use Enum.reduce/3 to convert a string into a map of character counts. For each character in the string, we'll build a new map with an updated key.
Here's how we would build the accumulator for the string "aba"
.
flowchart TB
A1["%{''a'' => 1}"]
A2["%{''a'' => 1, ''b'' => 1}"]
A3["%{''a'' => 2, ''b'' => 1}"]
subgraph Accumulator
direction TB
A1 --> A2 --> A3
end
subgraph Element
direction TB
a1[a] --> b --> a2[a]
end
We can initialize or update a value in a map using Map.update/4. If there is no existing key, it initializes the map key with the default value provided.
updated_map = Map.update(%{}, :key, "default value", fn current -> "updated: #{current}" end)
If there is an existing key, then it can update the map key using the existing value.
Map.update(updated_map, :key, "default value", fn current -> "updated: #{current}" end)
When converting a string to a map of character counts, if there is no key, the value will be 1
. If there is an existing key in the map, the value will be the current value incremented by 1
. Here are the steps we want to accomplish for our Enum.reduce/3 function.
initial_accumulator = %{}
step1 = Map.update(initial_accumulator, "a", 1, fn current_value -> current_value + 1 end)
step2 = Map.update(step1, "b", 1, fn current_value -> current_value + 1 end)
step3 = Map.update(step2, "a", 1, fn current_value -> current_value + 1 end)
Putting this all together, here's the Enum.reduce/3 function that lets us convert a string into a map with character counts. Note that we have to split the string into a list of characters first to make it enumerable.
split_string = String.split("aba", "", trim: true)
Enum.reduce(split_string, %{}, fn char, map_accumulator ->
Map.update(map_accumulator, char, 1, fn current -> current + 1 end)
end)
Use Enum.reduce/3 to convert the integer 1234321
into a map of digit counts.
%{1 => 2, 2 => 2, 3 => 2, 4 => 1}
Example Solution
digits = Integer.digits(1_234_321)
Enum.reduce(digits, %{}, fn integer, acc ->
Map.update(acc, integer, 1, fn current -> current + 1 end)
end)
One trick for reduce is to use a collection data-type to simulate having multiple accumulators.
Here's an example of separating a range into even and odd numbers.
Enum.reduce(1..10, {[], []}, fn integer, {evens, odds} ->
if rem(integer, 2) == 0 do
{[integer | evens], odds}
else
{evens, [integer | odds]}
end
end)
Technically, there is only one actual accumulator, our tuple with two lists. However, this functions as a way to track multiple values with each enumeration in the reduce operation.
Consider the following resource(s) to deepen your understanding of the topic.
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 Reduce 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.