pejrich

pejrich

Addition of a compact_map/2 function to the Enum module

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.

Most Liked

gregvaughn

gregvaughn

I’m kinda surprised no one has mentioned for comprehensions. When I think of map/flat_map with a filter, that’s where my mind goes. Of course, to some people’s taste, this may be even more golf-y than flat_map :person_shrugging: Here’s how I’d do it in one line, a single pass through the Enumerable, and no extra List.wrap:

iex(29)> for x <- [1, 2, 3, 4], y = x + 1, Integer.is_odd(y), do: y
[3, 5]

edit: I thought of a better example where we use a helper function that may return nil or not which fits more closely with the original situation

iex(30)> odd_to_nil = fn x -> if Integer.is_odd(x), do: nil, else: x end
#Function<42.105768164/1 in :erl_eval.expr/6>
iex(31)> for x <- [1, 2, 3, 4], y = odd_to_nil.(x), do: y
[2, 4]

The main piece to understand are filters in for comprehensions. If you have a clause that is not a generator, it’s a filter. If it evaluates to falsy, then the do block is not executed and the next iteration begins.

josevalim

josevalim

Creator of Elixir

filter in Rust works on predicates (booleans), while filter_map works on option types, which is different to Elixir’s previous filter_map.

My experience with compact in Ruby does not make me optimistic for making it part of the language. A lot of times it was added to remove nils from collection when the better question was to ask why nil is 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_ok and find_ok though, that explicitly look for ok tuples, but then we need to decide if we discard everything else or if we discard specific error results.

Thanks for starting this discussion!

sodapopcan

sodapopcan

I’m generally happy with status quo but would use it if it were available. Even though it makes sense and I can’t think of anything better, there’s something about the term “compact” that I never liked for this use-case, so in a weird way I’ve enjoyed being forced to “reject nils” in so many words.

Also, there is something off about a function that has “map” in its name that doesn’t return a list of the same size that went into it. Though there are probably examples I’m not thinking of… like map_reduce, though the original length map is still in there :thinking:

EDIT: I re-read the thread and had missed @dimitarvp’s message re: filter_map which I believe Elixir actually used to have? In any event, there is precedence when it comes to my differing-length concerns so I suppose it’s a moot point.

Last Post!

sodapopcan

sodapopcan

Ya, I’m not trying to say anything definitive here, just theorizing.

Again I can just say that it comes down to expressiveness and utility—and that I’ve just never liked the name compact :sweat_smile: I don’t actually agree that stuff like sum and uniq are bloat since they give a clear name to common reductions, even if not used all the time in certain types of problems. Sure sum can be written as reduce(list, &1 + &2) but the problem with that is that the word reduce on its own doesn’t tell very much at all—you have to read the body with very little context as to what’s going to happen. Code would be exhausting to read if everything was reduce, especially when reading quickly! I know you aren’t arguing for that, my point is just that reject is already very expressive and gives you a lot of context as to what will be in the body. reject(&is_nil/1) is really easy to read and IMO even more expressive than compact.

In any event, I’m just bikeshedding since my stance here is indifference. Ultimately, I wouldn’t care very much if this made it into the language!

Where Next?

Popular in Proposals: Ideas Top

hst337
Elixir compiler and language specification Purpose of the proposal Elixir language is in mature state and no breaking or heavy changes ar...
New
dkuku
This is a proposal to make the map key mismatch errors a bit better: Every time I have a typo It’s very challenging for me even when I u...
New
markevans
Hi! I feel like Phoenix is slightly missing a trick when it comes to front-end Javascript libraries like React, Svelte, etc. I feel tha...
New
dibok
Hi, I’m trying to use phoenix.js in my Qt QML project which has it’s own buildin JavaScript engine. Problem is that (what I googled so f...
New
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 ma...
New
nunobernardes99
On 1.8+, when we generate an authentication system with mix phx.gen.auth we can make use of magic link login which is amazing and a great...
New
ffloyd
The Problem Currently, if I define a struct in the following way: defmodule MyStruct do # Both x and y will have the FIXED values unti...
New

Other popular topics Top

baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
hariharasudhan94
Lets say I have map like this fetching from my database %{"_id" =&gt; #BSON.ObjectId&lt;58eb1a7a9ad169198c3dXXXX&gt;, "email" =&gt; ...
New
sen
Hi All, I set a environment variables in dev.exs , like below code. when i start server, how can i set the ${enable} value? thanks. d...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
AngeloChecked
What learn first? Rust or Elixir Hi Elixir community! I’m here because i want learn a new language. I’m a junior developer and mainly i ...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New

We're in Beta

About us Mission Statement