-
Notifications
You must be signed in to change notification settings - Fork 3.5k
{Map, Keyword}.replace/3 and replace!/3 #5917
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
|
lib/elixir/lib/keyword.ex
Outdated
| def put_new!(keywords, key, value) when is_list(keywords) and is_atom(key) do | ||
| case :lists.keyfind(key, 1, keywords) do | ||
| {^key, _} -> raise KeyExistsError, key: key, term: keywords | ||
| false -> [{key, value} | keywords] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use single whitespace before ->.
(https://github.com/lexmag/elixir-style-guide#expression-group-alignment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍, I really have a hard time with the lining up of = or -> like this. It makes incidental change a common place. If we want to add a new condition or change the name of a var we end up having to respect the entire group.
| {^key, _} -> [{key, value} | keywords] | ||
| false -> keywords | ||
| end | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this update the value, instead of adding a new pair?
adkron
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall I think the code looks great. I'd love to see some tests around it, or if we are using doctests I think we have an issues with iex> Keyword.put_existing([a: 1], :b, 2).
I'm not sure that I find exceptions to be that helpful in this case. I would rather have it return an :ok tuple or and :error tuple. Exceptions bubble up and I'm not sure how this would be an exceptional circumstance. Do you have a use case that shows why we need the exceptions?
lib/elixir/lib/keyword.ex
Outdated
| ## Examples | ||
| iex> Keyword.put_existing([a: 1], :b, 2) | ||
| [a: 1] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The example output needs to have the new key, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nevermind, I was confused. I now see that this is correct.
lib/elixir/lib/map.ex
Outdated
| def put_existing!(map, key, value) do | ||
| case has_key?(map, key) do | ||
| true -> put(map, key, value) | ||
| false -> raise KeyError, key: key, term: map |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is equivalent to the :maps.update/3 BIF
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, we should use the bif and inline it accordingly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So would this become:
case :maps.update(map, key, new_value) do
{:badkey, key} -> raise KeyError, key: key, term: map
map -> map
end?
| """ | ||
| @spec put_existing(map, key, value) :: map | ||
| def put_existing(map, key, value) do | ||
| case has_key?(map, key) do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it will be better to use the :maps.update/3 instad of :maps.put/3 BIF in here - it gives the runtime additional knowledge, we expect the key to be there, so it has more opportunities for optimisation.
|
One option we are considering is to rename |
|
@josevalim @michalmuskala I expect that the naming difference from
|
|
@Qqwy if that is your opinion, it means we should go with |
|
I prefer |
|
I like |
Agree on this. I like |
|
@Qqwy I think the behaviour of the corresponding functions in Map and Keyword should be such that: map |> Map.foo(...) |> Map.to_list == map |> Map.to_list |> Keyword.foo(...)holds, of course ignoring ordering issues and non-atom keyed maps. |
|
All right, I'll rewrite it to be Other changes to be done:
Would the function of that body then become: case :maps.update(map, key, new_value) do
{:badkey, key} -> raise KeyError, key: key, term: map
map -> map
end? Things to be discussed:
@adkron Because the non-! versions of the functions already return a 'plain' value, it is impossible to work with tuple-results and have the bang-versions return plain values + exceptions instead. So our hand is forced in that regard because of backwards-compatibility. |
|
Ok, so let's go with
No.
No.
No. |
|
@michalmuskala I agree that for the majority of cases, this is a nice property to respect, but AFAIK the reason that Keyword exists, is to allow the possibility of having duplicate entries. |
No worries! That's what Pull Requests are for, right? Having a distilled snippet of code that can be reviewed in a more practical way than a general proposal can. And: Thanks! I misread the Erlang documentation. I thought it returned a tuple, rather than raising a tuple. |
|
Next question about [a: 1, b: 2, a: 3]
|> Keyword.replace(:a, 4)What should the output now be?
|
- Map.put_existing/3 to Map.replace, - Map.put_existing!/3 to Map.replace! - uses :maps.update/3 underwater for the implementation of these two functions. - Removes Maps.put_new!/3
|
The same as for |
|
@Qqwy, I would expect both to be replaced. I don't like it, but that is my expectation. |
- Removes Keyword.put_new!/3 - Replaces Keyword.put_existing/3 with Keyword.replace/3 - Replaces Keyword.put_existing!/3 with Keyword.replace!/3 - Behaviour of the Keyword.replace/3 and Keyword.replace!/3 functions is altered so they delete _all_ other occurrences of an existing value.
lib/elixir/lib/exception.ex
Outdated
| end | ||
| end | ||
|
|
||
| defmodule KeyExistsError do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The name KeyExistsError feels "strange" to me. Maybe DuplicatedKeyError, but still a little bit strange. IDK 😕
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is going to be removed again, as it was only required for put_new/3.
|
Keyword lists should always be pre-pended to when adding or replacing something, it should not remove later ones, I am relying on that in quite a few areas. |
|
The current implementation follows the precedence set by |
It is the erlang way for proplists (which yes I know elixir calls them keyword lists but they've seemed conceptually similar, except requiring atom keys only), and I very much am relying on it in more than a few places... ^.^; Remember that Erlang's proplists have not only a (Side-note, erlang property lists also support not only |
|
@OvermindDL1 There both is |
lib/elixir/lib/map.ex
Outdated
| ## Examples | ||
| iex> Map.replace(%{a: 1}, :b, 2) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Examples need 4 spaces indentation.
| later occurrences are removed. | ||
| ## Examples | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Examples need 4 spaces indentation.
Exactly, hence why I am saying that |
|
My 2 cents: |
|
I'd opt for EDIT: Actually |
|
@lexmag There we go! |
|
❤️ 💚 💙 💛 💜 |
Culmination of this discussion on the Elixir Forum.
This Pull Request adds the following:Map.put_new!/3which raises aKeyExistsErrorif the key that should be inserted already exists.Map.put_existing/3which only puts a value in the given map if the key already existed.Map.put_existing!/3which only puts a value in the given map if the key already existed, and raises aKeyErrorif it did not.Keyword.put_new!/3which raises aKeyExistsErrorif the key that should be inserted already exists.Keyword.put_existing/3which only puts a value in the given keyword list if the key already existed.Keyword.put_existing!/3which only puts a value in the given keyword list if the key already existed, and raises aKeyErrorif it did not.KeyExistsErrorexception module.Note that
Keyword.put_existingandKeyword.put_existing!add a new value to the front of the keyword list without removing the original one. I expect that this is the behaviour we want, but I wanted to draw attention to it to make sure.After discussion below, this is changed to:
Map.replace/3which only puts a value in the given map if the key already existed.Map.replace!/3which only puts a value in the given map if the key already existed, and raises aKeyErrorif it did not.Keyword.replace/3which only puts a value in the given keyword list if the key already existed. All earlier values with the same key are deleted from the keyword list.Keyword.replace!/3which only puts a value in the given keyword list if the key already existed, and raises aKeyErrorif it did not. All earlier values with the same key are deleted from the keyword list.