Skip to content

Conversation

@Qqwy
Copy link
Contributor

@Qqwy Qqwy commented Mar 23, 2017

Culmination of this discussion on the Elixir Forum.

This Pull Request adds the following:

  • Map.put_new!/3 which raises a KeyExistsError if the key that should be inserted already exists.
  • Map.put_existing/3 which only puts a value in the given map if the key already existed.
  • Map.put_existing!/3 which only puts a value in the given map if the key already existed, and raises a KeyError if it did not.
  • Keyword.put_new!/3 which raises a KeyExistsError if the key that should be inserted already exists.
  • Keyword.put_existing/3 which only puts a value in the given keyword list if the key already existed.
  • Keyword.put_existing!/3 which only puts a value in the given keyword list if the key already existed, and raises a KeyError if it did not.
  • The afore-mentioned KeyExistsError exception module.
  • Documentation + Doctests for the above.

Note that Keyword.put_existing and Keyword.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/3 which only puts a value in the given map if the key already existed.
  • Map.replace!/3 which only puts a value in the given map if the key already existed, and raises a KeyError if it did not.
  • Keyword.replace/3 which 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!/3 which only puts a value in the given keyword list if the key already existed, and raises a KeyError if it did not. All earlier values with the same key are deleted from the keyword list.

@lexmag
Copy link
Member

lexmag commented Mar 23, 2017

put_new and put_new! pair behaves differently from !-paired functions, I'm not sure if it's good thing to break expectations in that case.
Let's probably have separate PR with put_new! addition so we can review and merge put_existing!/3 separately?

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]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

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
Copy link
Member

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?

Copy link
Contributor

@adkron adkron left a 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?

## Examples
iex> Keyword.put_existing([a: 1], :b, 2)
[a: 1]
Copy link
Contributor

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?

Copy link
Contributor

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.

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
Copy link
Member

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

Copy link
Member

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.

Copy link
Contributor Author

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
Copy link
Member

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.

@josevalim
Copy link
Member

One option we are considering is to rename put_existing to replace, put_existing! to replace! and no longer add put_new!. Any preference on the name?

@Qqwy
Copy link
Contributor Author

Qqwy commented Mar 23, 2017

@josevalim @michalmuskala I expect that the naming difference from put_existing and replace also describes the difference in behaviour between what the Keyword implementation should do:

  • For Keyword.put_existing it makes the most sense to me to put a new version of an already-existing key at the front of the list. (instead of replacing the value that existed further down the list).
  • For Keyword.replace it makes the most sense to me to replace the value where it is in the list, rather than to add a new tuple at the front.

@josevalim
Copy link
Member

@Qqwy if that is your opinion, it means we should go with replace rather than put_existing.

@adkron
Copy link
Contributor

adkron commented Mar 23, 2017

I prefer replace as I think I wouldn't have thought the docs were wrong with that wording. I think it is both concise and precise. 👍

@michalmuskala
Copy link
Member

I like replace more than put_existing, and I don't feel a huge need for put_new! - put_new itself is already rather rare use case, this one would be even rarer.

@lexmag
Copy link
Member

lexmag commented Mar 23, 2017

I don't feel a huge need for put_new! - put_new itself is already rather rare use case, this one would be even rarer.

Agree on this.

I like put_existing though, it forms nice group of put* functions and assists in searching.

@michalmuskala
Copy link
Member

@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.

@Qqwy
Copy link
Contributor Author

Qqwy commented Mar 23, 2017

All right, I'll rewrite it to be Maps.replace/3 and Keyword.replace/3. Keyword.replace/3 will replace the value in the keyword list, rather than pushing a new element to the front.

Other changes to be done:

  • Use :maps.update/3 for Map.replace/3 as @michalmuskala observed 👍 .

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:

  • Yes/no about adding put_new!.
  • Maybe it is not something that should be discussed as part of this PR, but if we pass a struct to replace (or put_existing), would it be possible to raise a compile-time error if a non-existing field name is used?

@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.

@josevalim
Copy link
Member

Ok, so let's go with replace and replace! and drop put_new!. Sorry for the back and forth @Qqwy.

Would the function of that body then become:

No. :maps.update already raises. :maps.update(key, val, map) is all you need.

Yes/no about adding put_new!.

No.

Maybe it is not something that should be discussed as part of this PR, but if we pass a struct to replace (or put_existing), would it be possible to raise a compile-time error if a non-existing field name is used?

No.

@Qqwy
Copy link
Contributor Author

Qqwy commented Mar 23, 2017

@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.

@Qqwy
Copy link
Contributor Author

Qqwy commented Mar 23, 2017

Sorry for the back and forth @Qqwy

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.

@Qqwy
Copy link
Contributor Author

Qqwy commented Mar 23, 2017

Next question about Keyword.replace/3: Say we do the following:

[a: 1, b: 2, a: 3] 
|> Keyword.replace(:a, 4)

What should the output now be?

  1. [a: 4, b: 2, a: 3]
  2. [a: 4, b: 2]

 - 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
@michalmuskala
Copy link
Member

The same as for Keyword.put, just raising if the key is not there.

@adkron
Copy link
Contributor

adkron commented Mar 23, 2017

@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.
end
end

defmodule KeyExistsError do
Copy link
Contributor

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 😕

Copy link
Contributor Author

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.

@OvermindDL1
Copy link
Contributor

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.

@Qqwy
Copy link
Contributor Author

Qqwy commented Mar 23, 2017

The current implementation follows the precedence set by Map.put as @michalmuskala noted for consistency's-sake, but I agree with @OvermindDL1 that only altering the front of the keyword list feels like it is the proper behaviour for keyword lists.

@OvermindDL1
Copy link
Contributor

OvermindDL1 commented Mar 23, 2017

The current implementation follows the precedence set by Map.put as @michalmuskala noted for consistency's-sake, but I agree with @OvermindDL1 that only altering the front of the keyword list feels like it is the proper behaviour for keyword lists.

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 get_value/2 call but also a get_all_values/2 call, where the first returns the first match and the second returns all matches in the entire proplist, this is highly useful functionality in many areas (like where I am using it for Elixir's Keyword lists).

(Side-note, erlang property lists also support not only {Key, Value} but also some_atom, which is short-hand for {some_atom, true}, I miss this shorthand in elixir, I use a lot of blah: true to compensate, but unsure how the syntax would work considering elixir's parser, hmm... EDIT: Maybe just supporting blah: inline as in foo(42, blorp: 86, blah:, floom: 21), or foo(blah:)?)

@Qqwy
Copy link
Contributor Author

Qqwy commented Mar 23, 2017

@OvermindDL1 There both is Keyword.get (newest value) and Keyword.get_values (all values) in the Keyword module.

## Examples
iex> Map.replace(%{a: 1}, :b, 2)
Copy link
Member

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
Copy link
Member

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.

@OvermindDL1
Copy link
Contributor

@OvermindDL1 There both is Keyword.get (newest value) and Keyword.get_values (all values) in the Keyword module.

Exactly, hence why I am saying that put_existing/3/replace/3 should prepend a value, those old values are needed sometimes as it is a multi-value map in essence. I could see the use of adding a replace_all/3 that removes all matching keys then prepends the new one on to the list though.

@whatyouhide
Copy link
Member

My 2 cents: replace is clearer in what it does than put_existing. put_* is already present yes, but put_existing just reads funny compared to the so clear replace :).

@OvermindDL1
Copy link
Contributor

OvermindDL1 commented Mar 23, 2017

I'd opt for replace for Map, but put_existing would be more accurate for Keyword I'd think.

EDIT: Actually put_existing/replace might not be necessary at all for Keyword as you can just put it on anyway, but replace_all would be useful.

@josevalim josevalim changed the title {Map, Keyword}.put_existing/3, put_existing!/3 and put_new!/3 {Map, Keyword}.replace/3 and replace!/3 Mar 23, 2017
@josevalim
Copy link
Member

@Qqwy we only need to address @lexmag comments about indentation and this is good to go!

@Qqwy
Copy link
Contributor Author

Qqwy commented Mar 23, 2017

@lexmag There we go!

@josevalim josevalim merged commit 3f88ab7 into elixir-lang:master Mar 23, 2017
@josevalim
Copy link
Member

❤️ 💚 💙 💛 💜

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

8 participants