Feature: Add Map.rename_key/3 and/or Map.rename_keys/3

170 views
Skip to first unread message

Jonathan Arnett

unread,
Aug 17, 2020, 5:35:18 PM8/17/20
to elixir-lang-core
I often times find myself in situations where I need to rename one or more keys in a Map, when for instance, converting from Strings to Atoms:

new_map = %{
  foo
: Map.fetch!(old_map, "foo"),
  bar
: Map.fetch!(old_map, "bar"),
 
...
}

or otherwise taking a Map from one part of the system and renaming keys to make it work in another part of the system:

new_map = %{
  user_id
: post.author_id,
  content
: post.body,
 
...
}

With a Map.rename_key/3 function, we could instead write:

new_map =
  old_map
 
|> Map.rename_key("foo", :foo)
 
|> Map.rename_key("bar", :bar)
 
...

or:

new_map =
  post
 
|> Map.rename_key(:user_id, :author_id)
 
|> Map.rename_key(:body, :content)
 
...

In the situation that you're performing a bulk update of key names (as seems to be implied above), Map.rename_keys/3 makes more sense:

new_map = Map.rename_keys(old_map, %{"foo" => :foo, "bar" => :bar, ...})

or:

new_map = Map.rename_keys(post, %{author_id: :user_id, body: :content, ...})

With the Map.rename_key[s]/3 functions, the intent is very explicit, and induces less cognitive load to understand.

Renaming Map keys is a situation I run into at least monthly, and I do wonder whether the rest of the community has this problem as well so as to make it a worthwhile addition.

Austin Ziegler

unread,
Aug 19, 2020, 2:53:29 PM8/19/20
to elixir-l...@googlegroups.com
I think that this can be done as a library. I’ve contributed to one https://github.com/byjpr/MapRewire that works pretty well. I’ve got another library that I need to extract from our source code and release as a package, but it could probably be extended (trivially) to allow key renames as well as its current function (safe atomization of map keys as well as case conversion).

-a

--
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-co...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-core/70108df1-6c5e-475b-8dbd-b4222e920e0bo%40googlegroups.com.


--

jb.am...@gmail.com

unread,
Oct 10, 2020, 8:58:02 AM10/10/20
to elixir-lang-core
I was going to suggest something very similar to `Map.rename_key/3`.

To add to what Jonar already said, I think that the big plus of a `Map.rename_key/3` would be the readability of pipe chains, specially when you have to do a few renames. For example:

```
# Before
map
|> Map.put(:new_key, map.old_key)
|> Map.delete(:old_key)

# After
map
|> Map.rename_key(:old_key, :new_key)
```

At the end of the day this is just a bit of syntactic sugar over functions that already exist, so I'm curious to know what the core team thinks about bringing something like this into the language.

I'm happy to open a PR if we think it makes sense to do this.

Kurtis Rainbolt-Greene

unread,
Oct 11, 2020, 2:02:11 AM10/11/20
to elixir-l...@googlegroups.com
It's more than syntax sugar, it takes a load off engineers by having its own docs, tests, and type checking. These sort of value ads are great for a language, lest you end up with multiple libraries that all have the same or similar functions with different names, docs, tests, etc.



--
Kurtis Rainbolt-Greene,
Software Developer & Founder of Difference Engineers

Jesse Claven

unread,
Oct 12, 2020, 7:02:43 AM10/12/20
to elixir-l...@googlegroups.com

I’ve implemented our own helper in our projects that does this and would be a fan of seeing it added to Map.

José Valim

unread,
Oct 12, 2020, 7:25:46 AM10/12/20
to elixir-l...@googlegroups.com
Hi everyone,

I thought I had commented on this thread but apparently I have not, so apologies for the delay.

I am not convinced about this functionality because I honestly do not find this:
new_map =
  post
  |> Map.rename_key(:user_id, :author_id)
  |> Map.rename_key(:body, :content)
clearer than this:

new_map = %{
  user_id: post.author_id,
  content: post.body,
  ...
}

Maybe if you want to keep the other keys as is, then "rename_key" can be handy, but even then, what happens if you add a new key to post? Does it automatically appear in the new_map? Or should it not?

Even if we say that "clearer" is personal, there are practical reasons for preferring the latter, such as the runtime can optimize it better (as all keys are literals and the map is not built dynamically), and it is easier to typecheck maps with known keys.

So my $.02 here is that this is not something I would necessarily endorse and, if you really want to rename only certain keys inside a map, you can do it with a helper function or by using put+drop on the desired keys.

Thanks!

Bruce Tate

unread,
Oct 12, 2020, 10:28:01 AM10/12/20
to elixir-l...@googlegroups.com
One composes; one doesn't. 

--
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-co...@googlegroups.com.


--

Regards,
Bruce Tate
CEO

jonar...@gmail.com

unread,
Oct 12, 2020, 11:49:52 AM10/12/20
to elixir-lang-core
> Maybe if you want to keep the other keys as is, then "rename_key" can be handy

This is the use case I had in mind. Let's say I have a map with 100 keys, but only need to rename one of them. In that event, today I would write something like this:

{val, new_map} = Map.pop(original_map, :original_name)
Map.put(new_map, :new_name, val)


Explicitly defining a new map with 99 keys matching exactly and only one changed would be a very large amount of effort, and brittle. With a Map.rename_key/2 function, I could write the following, which (to Bruce's point) would fit nicely in a pipe chain:

Map.rename_key(original_map, :original_name, :new_name)


> what happens if you add a new key to post? Does it automatically appear in the new_map? Or should it not?

The new key should appear in the new map. The only thing that Map.rename_key/3 (or Map.rename_keys/3) should do is exchange one key for another, without consideration for the rest of the map at all. I enjoy using maps where flexibility is more important than the guarantees that the strictness of structs. If I were converting one struct to another, I would explicitly map each key as in you example above

new_thing = %Thing{

  user_id: post.author_id,
  content: post.body,
  ...
}

The application of Map.rename_key[s]/3 is more for a proxy application, where data is being shuttled from one place to another with a minor modification. I may or may not know (or care) what the entirety of the map looks like, I simply know that if a given key is present it needs to be replaced with a different one.

Adam Lancaster

unread,
Oct 12, 2020, 3:37:36 PM10/12/20
to elixir-l...@googlegroups.com
You could compose a function like this:

```elixir
def rename_keys(map, key_mapping) do
  Enum.into(map, %{}, fn {key, value} -> 
    {Map.fetch(key_mapping, key), value}
  end)
end

%{my_map: :is_cool}
|> rename_keys(%{my_name: :my_new_map})

# => %{my_new_map: :is_cool}
```

If you don’t want to put / drop. That way you don’t have to spell out the 99-key map, and it’s easy to pipe together with other functions.

It’s also more efficient than the proposed rename_keys would be as you can rename multiple keys in one pass.

Adam


-- 
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-co...@googlegroups.com.

Fernando Tapia Rico

unread,
Jan 16, 2021, 8:24:52 AM1/16/21
to elixir-lang-core
I would like to give another spin to this proposal:

@doc """
Replaces `old_key` with `new_key` only if `old_key` already exists in `map`.

## Examples

  iex> Map.replace_key(%{a: 1, b: 2}, :a, :c)
  %{c: 1, b: 2}

  iex> Map.replace_key(%{a: 1}, :b, :c)
  %{a: 1}

"""
@spec replace_key(map, key, key) :: map
def replace_key(map, old_key, new_key) do
  case map do
    %{^key => value} ->
      map |> delete(old_key) |> put(new_key, value)

    %{} ->
      map

    other ->
      :erlang.error({:badmap, other})
  end
end

Reply all
Reply to author
Forward
0 new messages