Map.take is not a good name

Hi all! Just want to know if there is someone else thinking Map.take is not a very good name for function that returns subset of a map. It’s not an issue or something but I feel uncomfortable with that name. In ruby, for example, method with same functionality called slice. When I type Map.take([:a, :b]) I expect a list of values mapped to these keys. What do you think?

1 Like

You mean you would expect it to do something like this?

Enum.map(keys, fn key ->
   case Map.fetch(map, key) do
     {:ok, value} -> {key, value}
     _ -> nil
   end
 end)

Perhaps, but I think that the name take is not obviously indicating one or the other case, so I personally don’t think it’s a bad name. Maybe it could have been called Map.pick, just to differentiate it from Enum.take which takes a number of elements, but renaming it now would have much bigger disadvantages than advantages in my opinion.

2 Likes

I expect it like this:

take = fn map, keys ->
  Enum.map keys, & Map.get(map, &1)
end

a = %{a: 1, b: 2, c: 3}
take.(a, [:b, :c])
> [2, 3]

Well, I don’t think it has to be renamed. Just want to know what other people think about it.

I find it quite understandable. I’ve always found slice hard to remember, even after many years of Ruby

8 Likes

My point is that you take values from map. I think, slice is not ideal either. But in my opinion, it is better that take.

You also take a subset. Language is tricky :slight_smile:

7 Likes

The name take seems very natural to me. What you describe there could be done with %{a: 1, b: 2, c: 3} |> Map.take([:b, :c]) |> Map.values() (provided ordering is not important).

However, I have wanted a Map.values/2 that has an extra parameter to name which keys’ values you want.

5 Likes

Yes, I can, but I also can do it with several other ways. But oneliner is much prettier that 2 pipes.

That’s a very good idea!
Since Elixir already has Map.values/1 it could be a little effort to make that function.

1 Like

You want a one-liner? You asked the right person :grin:

def values(%{} = map, keys) when is_list(keys), do: for {k, v} <- map, k in keys, do: v
3 Likes

:+1:Like a charm

FYI, I’m 90% sure that map |> Map.take(keys) |> Map.values is gonna win from a performance perspective because those are both BIFs.

3 Likes

Just that would not guarantee that the order of the values is the same as keys.

4 Likes

Idk I personally like the name and found it intuitive when I first heard of it.

3 Likes

Good point! I’m actually not sure what the expectation of a Map.values/2 would be now that you mention it. Map.values/1 makes no particular guarantee about value order. I suppose Map.values/2 is a lot more useful though if it does do things in the order of the supplied keys.

3 Likes

OK, guys. As far as I understand everyone is satisfied with name take. What about values/2? Do I get it right that there will be a proposal to include that function?

I’m pretty happy with Map.take (and I use it all over the place). At least it’s consistent with Keyword.take.

My only tangentially-related wish is that Map.merge/2 let you use a keyword-list.

1 Like

You can use Enum.into for this:

iex(1)> [a: 1, b: 2] |> Enum.into(%{c: 3})
%{a: 1, b: 2, c: 3}

I like values/2. There will only be a proposal if someone makes one though. The official place to make proposals like this is the elixir-lang-core mailing list here https://groups.google.com/forum/#!forum/elixir-lang-core

You’ll want to reference this thread as a place where the initial conversation took place and some support for the idea found.

You can’t enum.into in a pipeline the same way :frowning:

I think a lot of what will influence whether someone finds the vocabulary intuitive will be based on experience in other languages. For instance, slice wouldn’t likely make sense to someone coming from JS, as that returns a list using index and length as arguments (same as Enum.slice/2).

In other languages, I have seen take() used exclusively in the context of lists; with a different method used for collections (eg. pick or pluck). Essentially, the deciding factor of take vs pick is whether the response includes keys.

In Elixir, take seems to be used for consistency, with the response being contextual (but matching the original type). For instance:

  • Enum.take returns a list with no keys
  • Keyword.take returns a keyword (list with keys)
  • Map.take returns a map

If Elixir were to apply the "take for no keys, and pick for keys" convention, then it might look like this:

Enum.take/2 returns a list with no keys

Enum.take(["foo", "bar", "baz"], 0) 
# ["foo"]

Keyword.take/2 returns a list with no keys

Keyword.take([foo: "bar"], [:foo])
# ["bar"]

Keyword.pick/2 returns a keyword list with keys

Keyword.take([foo: "bar", baz: "qux"], [:foo])
# [foo: "bar"]

Map.take/2 returns a list without keys

Map.take(%{foo: "bar", baz: "qux"}, [:foo])
# ["bar"]

Map.pick returns a map with keys

Map.pick(%{foo: "bar", baz: "qux"}, [:foo])
# %{foo: "bar"}

I think I could get behind that, but it can also be a little tricky since you’re not always getting back the same type that you started with (losing the consistency that the current implementation provides). :thinking:

Remember, the hardest part of writing code is knowing what to name things!

1 Like

One thing I’m realizing is underspecified about values/2: What should it do if you pass it a key that isn’t in the map? Raise? Return nil? Ignore it?

1 Like