pejrich
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.
Trending in Proposals: Ideas
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #library
- #deployment
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #podcasts
- #javascript
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #blog-post
- #elixirconf-us
- #elixir-ls
- #ai
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #hex
- #security
- #metaprogramming










Showing Posts 1 to 10- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
LostKobrakai
Imo
flat_mapis the operation to use here (even overreduce). What you’re trying to do it a subset of mapping to zero or more results per element on the original enumerable. Especially if you don’t use the capture shorthand this really isn’t that "code golf"y anymore.I’d love to see some some usecases where this would be particularly useful over the current a bit more generic apis. The example on the example implementation doesn’t have me particularly excited.
pejrich
The reason it’s code golf-y to me is because the only reason to wrap each item, and use flat_map is to get rid of the nil, which I would say is not exactly clear. It’s being wrapped and unwrapped to exploit a quirk of that combination of functions. I think if someone who was new to Elixir, or even had been using it for a while saw that code, they would take quite a long time to under the intention. You need to know that
nilwraps to[], and not[nil], as well as realizing that[1], [], [2]flat_maps to[1, 2]. It might be obvious when you think about it, but it is still sort of different than what someone probably first thinks of when they think of what you might useflat_mapfor.To me its sort of like
map |> Jason.encode!() |> Jason.decode!(keys: :atoms). This code has nothing really to do with encoding or decoding, it’s merely a somewhat easy way to convert string keys to atom keys, but that intention is not really clear from the code alone.Another example:
If someone doesn’t know the capitalization rules of the German Eszett, they’re likely to think it’s a noop. Sort of like wrapping and immediately unwrapping again.
The reason the example is not that exciting is because I tried to keep it in line with the other examples which are short and to the point. I’m not sure any of the documentation examples alone get me excited about a function.
Does that get you excited? For me it doesn’t. It just demonstrates in a different way what the words are trying to explain.
dimitarvp
Using
List.wrapis just because you want the code even shorter. IMO keeping the original:map+rejectis much clearer when read. Shorter is not always more readable.Shame we don’t have Rust’s
filter_mapthough, it would solve your problem right away.pejrich
My proposal is the fix that very thing. It appears that
filter_mapin Rust is exactly the same as thecompact_mapproposed here, and thecompactMapin Swift. Though I would say that naming is a bit obscure for Elixir wherefiltertypically implies filtering on a condition, but it looks like in Rustfilter()typically implies removing nils(or ratherNonein Rust)LostKobrakai
Enum.filter_mapexists, but is deprecated.josevalim
filterin Rust works on predicates (booleans), whilefilter_mapworks on option types, which is different to Elixir’s previousfilter_map.My experience with
compactin Ruby does not make me optimistic for making it part of the language. A lot of times it was added to removenils from collection when the better question was to ask whynilis being part of the list in the first place.|> Enum.reject(&is_nil/1)is clear enough without introducing a new verb (compact) for everyone to learn.I don’t rule out adding functions such as
filter_okandfind_okthough, that explicitly look foroktuples, but then we need to decide if we discard everything else or if we discard specific error results.Thanks for starting this discussion!
fmn
hi there Peter!
for what it’s worth, example you are citing never gets old to me and always gets me excited
apologies if i sound too harsh, but what is not getting me excited is a language with bloated syntax, and i am always looking at proposals like yours while keeping this in mind.
imagine how Elixir would look after accepting n such proposals, motivated and justified in similar way, over time t. when i go through this exercise or look at languages which “felt a victim” of syntax bloat, i think it’s harmful.
personally, i prefer to have more generic functions at core syntax level.
pejrich
I wasn’t aware of that function, but while it has the same name as the Rust function, it appears to be quite different. It’s a more generic filter. So you can do
Enum.filter_map(1..10, & &1 < 5, & &1 + 1), whereas the Rust function merely removesnil(actuallyNone, the Rust equivalent)pejrich
It doesn’t sound too harsh. I think it’s a valid opinion. But what is the dividing line of bloat vs value? The
Enummodule has by my count(Enum.__info__(:functions) |> length) 112 functions. I believe every one can be implemented withreduce. Is your argument that anything beyondreduceis bloat?fmn
this is exactly what i think we should not be doing: judging value of your proposal in a relative vacuum of 2-3 functions, like
reducewhich you mention.i would prefer to consider Elixir syntax and “built-in” functions as a whole.
this screen taken from José’s post is illustrating my sentiment pretty well, i think:
IMHO it’s easier to add stuff than remove from a language, and more verbose, complex, expressive syntax is, higher entry threshold for proposal as yours should be.