-
Notifications
You must be signed in to change notification settings - Fork 3k
stdlib/lists: Add key search functions for lists with maps #10508
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
stdlib/lists: Add key search functions for lists with maps #10508
Conversation
CT Test ResultsTests are running... https://github.com/erlang/otp/actions/runs/20666261664 Results for commit 3e795ab To speed up review, make sure that you have read Contributing to Erlang/OTP and that all checks pass. See the TESTING and DEVELOPMENT HowTo guides for details about how to run test locally. Artifacts
// Erlang/OTP Github Action Bot |
These seven new functions in the lists module are aligned with the semantics and principles of the corresponding existing key search functions for lists with tuples. A map with an arbitrary number of association pairs is used as search key. The first element in the list that is a map and contains association pairs comparing equal to all of the key's association pairs is considered to match the key. When an element in the list matches the key, the key search function performs the list operation it is set out to do and returns the result. If no element matches the key, the list is left unchanged in the return value or 'false' is returned.
3e795ab to
451efda
Compare
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.
Useful as your additions may be, there is a general reluctance to add even more functions to the already rather bloated lists module, so I guess this is unlikely to be accepted.
For that matter, it was IMO a (very old) mistake to add functions which operate on lists of specific elements to a module which (nominally at least) deals with lists of anything.
I left some inline comments nonetheless.
That all said, your naming scheme (key*_m) is kind of... off 😅. The _m part meaning "... for maps" appears nowhere else, neither in this nor any other module (that I know of). Also, key* doesn't seem to fit all too well here either, since the functions rather match one map against others instead of their keys.
lib/stdlib/src/lists.erl
Outdated
| MapList1 :: [Map], | ||
| MapList2 :: [Map], | ||
| Map :: map(). | ||
| keydelete_m(Key, [E | Tail]) when is_map(Key) andalso map_size(Key) > 0 -> |
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.
| keydelete_m(Key, [E | Tail]) when is_map(Key) andalso map_size(Key) > 0 -> | |
| keydelete_m(Key, [E | Tail]) when is_map(Key) -> |
While it may make not much sense to explicitly allow empty maps (which in this case boils down to "remove everything"), I don't think it should be explicitly forbidden.
Same for the other functions you added with your PR.
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 semantics of an empty map is actually "match any map" which boils down to "match the first map element in the list". In other words, "remove the first element" in this case.
Either way it makes no sense as you say. I just thought it'd be better to not allow an empty map as a key, but if you still think it should be allowed, maybe it should be described in the documentation?
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 understand the reasoning, but IMO we should not impose restrictions based on what (we think) makes sense or not. Ie, if users want to do it that way, we let them. (This is a doctrine which never failed me, at least 😜)
It would be a different story if we had to do something special/extra work to make pointless things possible, or if having pointless ways of doing things would stand in the way of performance or sth.
That said, before you do any changes based on my suggestions, wait and see if the additions from this PR will be accepted at all. As I said, that is not a given because of current (over-)abundance of functions in the lists module even now. IIRC, I had own and seen other peoples' PRs rejected because of this.
lib/stdlib/src/lists.erl
Outdated
| MapList2 :: [Map], | ||
| Map :: map(). | ||
| keydelete_m(Key, [E | Tail]) when is_map(Key) andalso map_size(Key) > 0 -> | ||
| try maps:with(maps:keys(Key), E) == Key of |
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 don't think it is necessary to wrap each iteration step in a try ... catch. Key is already tested for mappyness (is that a word? 🤔), so you only need to test E for mappyness in each iteration step. tl;dr, it may be better to test Key in the (exported) API function, and run the actual keydelete loop in a helper (not exported) function which guards for mappyness of each E.
Same for the other functions you added with your PR.
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.
Agree.
| -doc(#{since => <<"OTP 29.0">>}). | ||
| -spec keysort_m(KeyElement, MapList1) -> MapList2 when | ||
| KeyElement :: term(), | ||
| MapList1 :: [Map], | ||
| MapList2 :: [Map], | ||
| Map :: map(). | ||
| keysort_m(KeyElem, MapList) -> | ||
| keysort_m(KeyElem, MapList, []). | ||
|
|
||
| keysort_m(KeyElem, [E | Tail], AccSort) -> | ||
| NewAccSort = keysort_m3(AccSort, E, KeyElem), | ||
| keysort_m(KeyElem, Tail, NewAccSort); | ||
| keysort_m(_, [], AccSort) -> | ||
| AccSort; | ||
| keysort_m(_, _, _) -> | ||
| erlang:error(badarg). | ||
|
|
||
| keysort_m3([SortedE | Tail], E, KeyElem) -> | ||
| try maps:get(KeyElem, E) < maps:get(KeyElem, SortedE) of | ||
| true -> | ||
| [E, SortedE | Tail]; | ||
| false -> | ||
| [SortedE | keysort_m3(Tail, E, KeyElem)] | ||
| catch | ||
| _ : _ -> | ||
| [SortedE | keysort_m3(Tail, E, KeyElem)] | ||
| end; | ||
| keysort_m3([], E, _) -> | ||
| [E]. |
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.
If I read this right (ie, I didn't try it out), elements which are not a map are ignored/dropped from the result list. This is different from what keydelete/3 does, which raises an error if an element is not a tuple or not a tuple of sufficient size.
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.
First of all, lists:keydelete/3 ignores anything that does not match the key criteria. No matter which term it is:
119> lists:keydelete(h, 1, [{}, oo, {h,1}]).
[{},oo]
The only restriction is from the spec:
-spec keydelete(Key, N, TupleList1) -> TupleList2 when
Key :: term(),
N :: pos_integer(),
TupleList1 :: [Tuple],
TupleList2 :: [Tuple],
Tuple :: tuple().
My key*_m functions have the same spec- and run principles as the existing ones.
This is the sort function which follows that same philosophy. This is a matter of changing the order of elements. If an element does not match the Key, the element shall not change place in the list by itself. Only those elements that match the key specification shall change their positions in relation to the other matching elements. Note that the key in this case is not the same Key as in the other functions. This Key is a map's key element of an association pair. In a homogenous list there should only be maps and all of them should have an association pair where the Key matches. But why not allow the list to contain other elements as well. The sorting algorithm only applies to matching elements. Leave the rest in peace :-)
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.
Again, I understand the reasoning and motivation. But new functions should follow the semantics of the existing ones, the ones they resemble the most (which, in the case of keysort_m, would be keysort, not keydelete).
I am aware that the semantics of the existing key* functions are inconsistent (keydelete ignores non- and too short tuples as you pointed out, while keysort raises an error). However, we can't change the semantics of the existing functions without running the risk of breaking a lot of existing code out there. Ie, they have been this way since, like, forever 🤷♀️
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'll be happy to get everything consistent, clean, logical and whatever good there is to attach to these functions in a dialog with the "boss"es of J.A's legacy if a positive decision comes. Until then, I'll just relax and wish for further expansion of the Erlang community.
| keydelete_m/1, keyfind_m/1, keymember_m/1, keyreplace_m/1, | ||
| keysort_m/1, keystore_m/1, keytake_m/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.
| keydelete_m/1, keyfind_m/1, keymember_m/1, keyreplace_m/1, | |
| keysort_m/1, keystore_m/1, keytake_m/1, | |
| keydelete_m/1, keyfind_m/1, keymember_m/1, keyreplace_m/1, | |
| keysort_m/1, keystore_m/1, keytake_m/1, |
Indendation.
|
These functions are indeed useful. That's why I wrote five of them at first. Then added two more when I realized they could possibly take a place in the lists module. From one perspective I understand that even the existing key search functions for tuples have a doubtful place in the more general lists module. However, for old sin's sake, I nonetheless propose to add the maplist functions since they are similar to the tuplelist functions. I don't see any better place at this time. These functions are in any case operating on elements in a list regardless of being specialized on certain erlang terms or in general. The naming scheme derives from the need to separate them from the existing Regarding key vs. match, I'd hold on to the key naming because of the essence of my proposed functions; The fundament for these functions is this:
|
Well, I agree... That said, let's see what the OTP team thinks 🤷♀️ |
|
Thanks for your pull request! We've decided not to adopt this as-is, since we believe it's more useful to add variants with predicates that would support any kind of data structure in anticipation of native records (OTP 29/30). |
These seven new functions in the lists module are aligned with the semantics and principles of the corresponding existing key search functions for lists with tuples. A map with an arbitrary number of association pairs is used as search key. The first element in the list that is a map and contains association pairs comparing equal to all of the key's association pairs is considered to match the key. When an element in the list matches the key, the key search function performs the list operation it is set out to do and returns the result. If no element matches the key, the list is left unchanged in the return value or 'false' is returned.