When dealing with collections, I frequently find myself filtering and mapping a collection to extract the results I need. Elixir’s Enum
library has a great filter_map/3
function to simplify this…
The general concept of filter_map
is pretty straight forward, but the first time I came across it I didn’t quite grok it straight away. I thought it may help others to explain in a bit more detail how to use it.
Deprecated [update 12/08/17]
Enum.filter_map/3
has been deprecated in Elixir 1.5:
Deprecate Enum.filter_map/3 in favor of Enum.filter/2 + Enum.map/2 or for-comprehensions
I’ve added some ‘Alternative Implementations’ to the examples below using Enum.filter/2
+ Enum.map/2
, or for-comprehensions.
Definition
Here’s the definition from hexdocs:
filter_map(enumerable, filter, mapper)
Filters the enumerable and maps its elements in one pass
And the example:
iex> Enum.filter_map([1, 2, 3], fn(x) -> rem(x, 2) == 0 end, &(&1 * 2))
[4]
Explanation
The example uses the &
shorthand for an anonymous function, but when you’re new to the language it can be a little confusing. Let’s re-write it without the shorthand:
Enum.filter_map(
[1, 2, 3], # enumerable
fn(value) -> rem(value, 2) == 0 end, # filter
fn(filtered_value) -> filtered_value * 2 end # mapper
)
Here’s what’s happening:
1. enumerable
This is our enumerable collection that we begin with, in this case it’s [1,2,3]
.
2. filter
This is a function defining how we want to filter the values. The filter function should return a truthy/non-truthy (everything is truthy except nil
and false
) result for each value. Only values that return a truthy value for the filter function will pass through.
In this case we want values from the collection that are divisible by 2. This function will be applied to each value in our enumerable.
fn(value) -> rem(value, 2) == 0 end
The only value in our enumerable that will return true
for this function is 2
, so our filter returns [2]
.
3. mapper
This is a function that will be applied to each value of the filter results, and returns the results in a list.
fn(filtered_value) -> filtered_value * 2 end
This function just multiples each value by 2, so it transforms the filter results [2]
to return [4]
.
Alternative Implementations
Here’s how the example above could be implemented using Enum.filter/2
+ Enum.map/2
:
[1,2,3]
|> Enum.filter(fn(value) -> rem(value, 2) == 0 end)
|> Enum.map(fn(filtered_value) -> filtered_value * 2 end)
Or it could also be written using a for
comprehension:
for value <- [1, 2, 3], rem(value, 2) == 0, do: value * 2
Further example
Here’s a slightly more complicated example. We’ve got a list of Structs containing user information:”
users = [
%{ id: 1, full_name: "John Doe", username: "jdoe", email: "[email protected]"},
%{ id: 2, full_name: "Mary Doe", email: "[email protected]"},
%{ id: 3, username: "dfaker", email: "[email protected]"},
%{ id: 4, email: "[email protected]"}
]
Let’s say we want a list of tuples in the form {id, name}
from our list of users, and we’ve got the following business logic:
name
can be the usersfull_name
orusername
- if they have both, prioritize thefull_name
- we want the names in uppercase
- if a user has no
full_name
orusername
we return nothing for that user
There many different ways we to do this, but one way, using Enum.filter_map
could look like this:
defmodule FilterMapExample do
def filter_users(users) do
Enum.filter_map(
users,
fn(user) -> has_name?(user) end,
fn(user) -> filterd_data(user) end
)
end
defp has_name?(%{full_name: _full_name}), do: true
defp has_name?(%{username: _username}), do: true
defp has_name?(_), do: nil
defp filterd_data(%{id: id, full_name: name}), do: {id, String.upcase(name)}
defp filterd_data(%{id: id, username: name}), do: {id, String.upcase(name)}
end
We’ve pulled out our filter and mapper functions into private methods has_name?
and filtered_data
and use pattern matching to return the correct result from these private methods.
Using the example to filter the users collection we get a list of tuples containing only users with a full_name/username in uppercase:
iex> FilterMapExample.filter_users(users)
[{1, "JOHN DOE"}, {2, "MARY DOE"}, {3, "DFAKER"}]
Simplifying filter_user
Using the &
shorthand and |>
, the filter_users
methods could be simplified:
def filter_users(users) do
users
|> Enum.filter_map(&has_name?/1, &filterd_data/1)
end
Alternative Implementations
Here’s how the filter_users/1
above could be implemented using Enum.filter/2
+ Enum.map/2
:
def filter_users(users) do
users
|> Enum.filter(&has_name?/1)
|> Enum.map(&filterd_data/1)
end
Or it could also be written using a for
comprehension:
def filter_users(users) do
for user <- users, has_name?(user), do: filterd_data(user)
end
It’s a handy method for working with collections, but going forward you should use Enum.filter/2
+ Enum.map/2
or for-comprehensions instead. Either way, I hope this helps you understand how to use filter_map
a little better!