Enum Looping Shortcuts
Looping using recursion typically won’t be your first choice when you need to loop. Sometimes using explicit recursion really is the best approach for a situation and it is very beneficial to understand how to do it. However, Elixir provides a number of helpful shortcut functions we use when we need to loop. Many of them are in the Enum
module. The Enum
module defines many functions for working with things that are “enumerable”. It can work on lots of things, not just lists. That’s why these functions aren’t on the List
module.
The top 3 functions we want to start with are:
Enum.each/2
– Invokes a function for each element in the enumerable. Returns:ok
.Enum.map/2
– Returns a list where each element is the result of invoking a function on each element of enumerable.Enum.reduce/3
– Invokes a function for each element in the enumerable with an accumulator.
Contents
A common mistake with Enum.each/2
Coming from an imperative language, it might be your knee jerk reaction to reach for Enum.each/2
. Doing that would be a mistake. It probably isn’t what you actually want.
Let’s look at a Javascript example of how many people use OOP & imperative patterns to loop. Even if Javascript isn’t your thing, something like this probably feels familiar.
var data = [
{name: "Customer 1", total: 0},
{name: "Customer 2", total: 100},
{name: "Customer 3", total: 200},
];
data.forEach(function(customer) {
customer.total += 50;
});
data
//=> [{name: "Customer 1", total: 50},
//=> {name: "Customer 2", total: 150},
//=> {name: "Customer 3", total: 250}]
People coming to functional languages like Elixir get confused and frustrated when they try to do something similar in Elixir. Let’s see how people might initially try to do the same operation in Elixir.
data = [
%{name: "Customer 1", total: 0},
%{name: "Customer 2", total: 100},
%{name: "Customer 3", total: 200},
];
Enum.each(data, fn(customer) ->
Map.put(customer, :total, customer.total + 50);
end)
data
#=> [
#=> %{name: "Customer 1", total: 0},
#=> %{name: "Customer 2", total: 100},
#=> %{name: "Customer 3", total: 200}
#=> ]
The developer trying this probably got pretty frustrated. To top it all off, the resulting data isn’t even updated with the incremented totals!
Let’s assume the developer is trying to create a 1:1
conversion of how they’d solve the problem in Javascript but now using Elixir.
First of all, they’d try to use +=
, but Elixir doesn’t have a +=
operator because by definition, that mutates a variable. Bam! <Head hits wall>
Second, then the developer might write something like customer.total = customer.total + 50
. Attempting this will cause a syntax error because customer.total
is not a pattern and it’s on the left side of the match operator =
which expects pattern = data
. Bam! <Head hits wall>
Lastly, after figuring out what they couldn’t do, they tried Map.put/3
on the data and it still didn’t work. Bam! Man, I feel for that poor, frustrated developer. Thankfully, that won’t be you!
The reason the last Map.put/3
operation didn’t work as expected is because it return a new map with the value updated. The code above creates an updated map and then promptly discards the newly created map. Unintentionally of course. The developer making this mistake was still expecting data mutation. They reached for the Enum.each/2
function because that seemed to match with what they would do in an imperative language.
What the developer actually wanted was Enum.map/2
.
Using Enum.map/2
If you want to update the things in a list, then you probably want Enum.map/2
. Remember in Elixir we are working with immutable data. So if I want to update a list of numbers, what I actually want is a new list of numbers that are updated in the way I want.
To update a list of numbers adding 10 to every number, I could write:
data = [1, 2, 3, 4, 5]
updated = Enum.map(data, fn(val) -> val + 10 end)
updated
#=> [11, 12, 13, 14, 15]
In this example, Enum.map/2
takes two arguments. The first is an enumerable list. The second is a function to perform on each element in the list. The result of Enum.map/2
is a new list with the same number of elements as the list going in, but each element in the new list is the value returned by the function.
Exercise #1 – Fixing the Example
The example the frustrated developer was working on needs to get fixed! We can’t leave the code unfinished! Use Enum.map/2
to create an updated_data
variable. Your goal is to update the total
in each map by increasing it by 50
. Use IEx to try out your solution. You can write it in a text editor for convenience.
data = [
%{name: "Customer 1", total: 0},
%{name: "Customer 2", total: 100},
%{name: "Customer 3", total: 200},
];
Anonymous function shorthand
Remember that Elixir supports a shorthand for writing anonymous functions. This is relevant because all the Enum
functions we are working with take a function as an argument. It is very common to see and use the shorthand version. It’s worth spending some time getting comfortable with it. The following are equivalent:
Enum.map([2], fn(val) -> val * 2 end)
#=> [4]
Enum.map([2], &(&1 * 2))
#=> [4]
In the shorthand function, the first &
means “this is an anonymous function”. The &1
means, “this is the first argument passed to the function”. Similarly, &2
stands in for the second argument, &3
the third argument and so forth. To compare the two functions, the &
means fn
and &1
is the same as val
. Also notice that no end
is used for the anonymous function.
Another point to note is that parenthesis are optional in Elixir. So the following two statements are equivalent as well.
Enum.map([2], &(&1 * 2))
#=> [4]
Enum.map([2], & &1 * 2)
#=> [4]
The reason I point this out is because the Elixir formatter removes parenthesis when they aren’t needed. So you may end up seeing this on your own code even if you didn’t write it that way.
Exercise #2 – Fixed Example using Shorthand
Rewrite the fixed Enum.map/2
version from Exercise #1 using the function shorthand. Use IEx to try out your solution. You can write it in a text editor for convenience.
Using local functions
Another approach is to use local module functions as the function being used in the Enum.map/2
call. Here’s an example of what that might look like:
defmodule Playing do
def values_doubled(list) do
Enum.map(list, &doubler/1)
end
defp doubler(val), do: val * 2
end
Playing.values_doubled([1, 2, 3])
#=> [2, 4, 6]
The module defines a function doubler/1
that takes a single value, multiplies it by 2 and returns the result. The function values_doubled/1
passes doubler
as a function reference which identifies the function using its name and arity. Writing the function this way looks similar to &doubler(&1)
but this form is actually executing the function, not passing a reference to it. In total it looks like this:
defmodule Playing do
def values_doubled(list) do
Enum.map(list, &doubler(&1))
end
defp doubler(val), do: val * 2
end
Playing.values_doubled([1, 2, 3])
#=> [2, 4, 6]
A benefit to using a function declared in a module (either passed by reference or executed directly) is that it makes it easier to use pattern matching in the function doing the work. As a slightly absurd example, this works:
defmodule Playing do
def values_doubled(list) do
Enum.map(list, &doubler(&1))
end
defp doubler(val) when is_number(val), do: val * 2
defp doubler(val) when is_binary(val), do: val <> val
end
Playing.values_doubled([1, 2, 3])
#=> [2, 4, 6]
Playing.values_doubled(["Hi", "Hello"])
#=> ["HiHi", "HelloHello"]
I hope you get the point that you have options with what you pass to an Enum.map/2
call for the function it uses to operate on the elements of an enumerable. Of course, this applies to other Enum
calls as well.
Ranges
This is a good time to talk about another data type in Elixir. It’s called a “range“. A range is most often expressed using this syntax: 1..10
. This range represents the range of numbers from 1 up to and including 10. When we want to perform an operation a specific number of times, then a range can be helpful! Let’s see an example:
Enum.each(1..5, fn(num) ->
IO.puts "Processed num #{num}"
end)
#=> Processed num 1
#=> Processed num 2
#=> Processed num 3
#=> Processed num 4
#=> Processed num 5
#=> :ok
It is worth pointing out here that the no matter the size of the range (ex 1..1_000_000
), an Enum
module function will be memory efficient because it uses logic to iterate. Stated another way, an Enum
function doesn’t fully expand the range into a list, it uses logic and the range’s endpoint values to iterate.
If you want to see what a range includes, you can use Enum.to_list/1
to expand it.
Enum.to_list(1..5)
#=> [1, 2, 3, 4, 5]
Enum.to_list(1..0)
#=> [1, 0]
Enum.to_list(1..-5)
#=> [1, 0, -1, -2, -3, -4, -5]
Enum.to_list(1..1)
#=> [1]
Notice that the range can reverse order and go from high to low as well. The important point with ranges and Enum
functions is that you can never get an empty list. A range of 0..0
expands to [0]
. So if you want to use a range and a variable for the high-end, you may need to explicitly handle receiving a 0
. An example will help make this clear.
defmodule Playing do
def counting(up_to_number) do
Enum.each(1..up_to_number, fn(num) ->
IO.puts "Counting: #{num}"
end)
end
end
# this is as you'd expect...
Playing.counting(5)
#=> Counting: 1
#=> Counting: 2
#=> Counting: 3
#=> Counting: 4
#=> Counting: 5
#=> :ok
# this may surprise you...
Playing.counting(0)
#=> Counting: 1
#=> Counting: 0
#=> :ok
The lesson to take away is that using a range with Enum
functions work best when the range is developer declared. Not necessarily when taking arbitrary input. The alternative is to explicitly handle receiving something like a 0
.
Using Enum.each/2
So what is Enum.each/2
actually good for? It’s great when you want to iterate and perform some operation but don’t care about the result. I mean you don’t care about the result of Enum.each/2
because it always returns :ok
. You may want the side-effects that you create during the iterations, but you aren’t creating a modified list and returning it.
This is a great time to practice doing that!
Exercise #3 – Creating Customers
A previous exercise using recursion can now be re-implemented using Enum.each/2
. The objective here is to create a set number of new customer entries. Use the function CodeFlow.Fake.Customers.create/1
to perform the customer create operation. In order to create a valid customer, you must at least provide a name. Again, the focus here is on loop control.
mix test test/enum_shortcut_test.exs --only describe:"create_customers/1"
A good place to pause
We have already covered quite a bit! You may feel a bit overwhelmed. However don’t worry about it! These are a lot of new concepts you’re being exposed to.
Before we end this lesson, let’s review a bit about what we just covered.
- We saw some common mistakes people make when first using
Enum
. - We looked at 2 important
Enum
functions. - We again covered using anonymous functions.
- We looked at how to use local functions.
- We also introduced Ranges.
In the next lesson, we’ll go deeper with Enum
. We’ll look at Enum.reduce/3
and see different things we can do with anonymous functions. We will also see how the Enum
functions can enumerate over more than just a list!
Comments are closed on this static version of the site.
Comments are closed
This is a static version of the site. Comments are not available.