I propose adding compact_map/2 to the Enum module.
What is it?
Sometimes you want to map over a collection, but sometimes you want to map over a collection keeping only what’s not nil.
Can you do it now?
Yes, you can definitely already do it. You have some options.
Most common, and shortest is:
Enum.map(list, &fun/1)
|> Enum.reject(&is_nil/1)
The reason I don’t love this code, is because mentally I don’t think of it as a map step, and a separate filter step, and when reading the code the reject step feels a bit like noise. Sure if you are rejecting anything less than 3, that’s a part of the logic that should be explicit. But mapping without the nils in my mind feels more like a particular flavor of mapping, rather than two separate steps. These two steps also mean an additional pass over the list. You can do it more efficiently(about 2.5-3.5x more efficiently based on some quick benchmarks), but it requires breaking out reduce/3, which while it’s certainly the most powerful of the Enum functions, it can sometimes feel verbose for simpler things:
Enum.reduce(list, [], fn element, acc ->
case fun.(element) do
nil -> acc
result -> [result | acc]
end
end)
|> Enum.reverse()
It feels like a mouthful for just mapping without the nils.
There is a single line solution using just stdlib functions, but this is peak code golf/“just because you can, doesn’t mean you should” level coding. Really smelly stuff to me:
Enum.flat_map(list, &List.wrap(fun.(&1)))
Why not just put it in your own module?
I generally do have a number of functions that are additions to Enum, and it can sometimes be annoying to remember some functions are Enum functions, and others are in some weirdly named module that wanted to be called Enum but couldn’t because Enum™ was already in use. But to me compact_map/2 feels like it’s a core enough functionality that it deserves to be in Enum™ and not some straight-to-DVD knock off MyEnum
Is anyone else doing it? Anyone cool?
Apple is doing it. A lot of people think they’re cool(and other people hate them because so many people think they’re cool).
You marked this as “Easy”. Is that just because you’re hoping someone else will do the work, so it’ll be easy for you?
No. I’m happy to do the work. Here’s an implementation. I think i’ve followed all the current Enum naming conventions, and i’m happy to add some tests, or alter it if there are any suggestions. Or maybe people hate the whole idea, that’s what I hope this post will determine.
@doc """
Returns a list where each element is the result of invoking
`fun` on each element of `enumerable`,
where the result of `fun` is not `nil`.
## Examples
iex> lookup = %{a: 1, c: 3}
iex> Enum.compact_map([:a, :b, :c, :d], &lookup[&1])
[1, 3]
"""
@spec compact_map(t, (element -> any)) :: list
def compact_map(enumerable, fun) do
reduce(enumerable, [], fn element, acc ->
case fun.(element) do
nil -> acc
result -> [result | acc]
end
end)
|> :lists.reverse()
end
I think it’s cool. Apple thinks it’s cool. Do you think it’s cool?
Moderators, sorry about the double post, my first one had the text deleted when I posted it, and I didn’t want to leave a mangled post up while I rewrote it, so I deleted it, but it seems to still be visible.























