pejrich

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.

Showing Posts 1 to 10

LostKobrakai

LostKobrakai

Imo flat_map is the operation to use here (even over reduce). 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.

Enum.flat_map(list, fn el -> List.wrap(fun.(el)) end)

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

pejrich OP

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 nil wraps 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 use flat_map for.

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:

"ß" |> String.downcase() |> String.upcase()

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.

iex> Enum.map([1, 2, 3], fn x -> x * 2 end)
[2, 4, 6]

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

dimitarvp

Using List.wrap is just because you want the code even shorter. IMO keeping the original: map + reject is much clearer when read. Shorter is not always more readable.

Shame we don’t have Rust’s filter_map though, it would solve your problem right away.

pejrich

pejrich OP

Shame we don’t have Rust’s filter_map though, it would solve your problem right away.

My proposal is the fix that very thing. It appears that filter_map in Rust is exactly the same as the compact_map proposed here, and the compactMap in Swift. Though I would say that naming is a bit obscure for Elixir where filter typically implies filtering on a condition, but it looks like in Rust filter() typically implies removing nils(or rather None in Rust)

LostKobrakai

LostKobrakai

@deprecated “Use Enum.filter/2 + Enum.map/2 or for comprehensions instead”

Enum.filter_map exists, but is deprecated.

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!

fmn

fmn

hi there Peter!

for what it’s worth, example you are citing never gets old to me and always gets me excited :smiley:

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

pejrich OP

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 removes nil(actually None, the Rust equivalent)

pejrich

pejrich OP

It doesn’t sound too harsh. I think it’s a valid opinion. But what is the dividing line of bloat vs value? The Enum module has by my count(Enum.__info__(:functions) |> length) 112 functions. I believe every one can be implemented with reduce. Is your argument that anything beyond reduce is bloat?

fmn

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 reduce which you mention.

i would prefer to consider Elixir syntax and “built-in” functions as a whole.

  • how verbose it (Elixir) is already?
  • what is the cognitive load associated with language learning, usage and maintaining code written in it?
  • how many ways we have already to do something?
  • is language syntax flat or deep?

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.

Where Next? Top

Trending in Proposals: Ideas Top

woylie
We are seeing a lot of warning logs like this: navigate event to "https://someurl" failed because you are redirecting across live_sessio...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews