Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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
Sequence
grouped(by:)andkeyed(by:)#2078Sequence
grouped(by:)andkeyed(by:)#2078Changes from all commits
c19f3f1File filter
Filter by extension
Conversations
Jump to
There are no files selected for viewing
Sequence
grouped(by:)andkeyed(by:)Introduction
This proposal would add new APIs on
Sequencewhich let you group up or key elements in a more natural and fluent way than is currently possible withDictionary's initializers.Motivation
SE-0165 Dictionary & Set Enhancements introduced some great utility APIs to Dictionary. Relating to this proposal are these 3 initializers:
Dictionary.init(grouping:by:)Dictionary.init(uniqueKeysWithValues:)Dictionary.init(_:uniquingKeysWith:)These APIs have proven very useful, and have replaced a plethora of hand-rolled calls to
reduce(into: [:]) { ... }, but as initializers, they have some usability short comings.Grouping
It's not uncommon that you would like to group the results of a chain of method calls that transform a Sequence, and perhaps even keep chaining more transformations onto that Dictionary. For example:
The initializer breaks down the simple top-to-bottom flow, and makes readers need to scan from the middle, to the top, and back to the bottom to follow the flow of transformations of values.
If this same capability existed as a method on
Sequence, then the code can be much more fluent and prose-like:Keying by a value
Many usages of
Dictionary.init(uniqueKeysWithValues:)andDictionary.init(_:uniquingKeysWith:)are expressing the idea of creating a Dictionary of values keyed by the some key (typically derived from the values themselves). Many such uses can be found where these initializers are paired with a call tomap. This introduces syntactic complexity and an intermediate Array allocation, if the author doesn't remember to call.lazy.map.This initializer is pretty syntactically heavy, its combination with
mapis a non-obvious pattern, and it suffers from the same reading up-and-down problem as the grouping case.This concept could be expressed more clearly, and the intermediate array allocation can be spared, if this was a method on
Sequence:Prior art
groupingBytoMapgroupByassociatedByGroupByToDictionarygroup_bygroup_byindex_bygroupbygroupBykeyByProposed solution
This proposal introduces 2 new methods on
Sequence. Here are simple examples of their usages:Detailed design
Source compatibility
All the proposed additions are purely additive.
ABI compatibility
This proposal is purely an extension of the standard library which can be implemented without any ABI support.
Implications on adoption
TODO
The compatibility sections above are focused on the direct impact of the proposal on existing code. In this section, describe issues that intentional adopters of the proposal should be aware of.
For proposals that add features to the language or standard library, consider whether the features require ABI support. Will adopters need a new version of the library or language runtime? Be conservative: if you're hoping to support back-deployment, but you can't guarantee it at the time of review, just say that the feature requires a new version.
Consider also the impact on library adopters of those features. Can adopting this feature in a library break source or ABI compatibility for users of the library? If a library adopts the feature, can it be un-adopted later without breaking source or ABI compatibility? Will package authors be able to selectively adopt this feature depending on the tools version available, or will it require bumping the minimum tools version required by the package?
If there are no concerns to raise in this section, leave it in with text like "This feature can be freely adopted and un-adopted in source code with no deployment constraints and without affecting source or ABI compatibility."
Alternatives considered
Wait for a "pipe" operator, and just use the existing initializers
The general issue here is the ergonomics of free functions (and similarly, initializers and static functions), and how they don't chain together as nicely as instance functions. There has been community discussion around introducing a generalized solution to this problem, usually an Elixir-style pipe operator,
|>.This operator takes the value on its left, and passes it as the first argument to the function passed to its right. It might look like so:
This composes nicely, and reuses the existing Dictionary initializer, but brings its own challenges.
Dictionary(grouping:by:)takes two arguments, but the|>operator would expect a right-hand-side closure that takes only 1 argument. Resolving this requires one of several approaches, each with some downsides: 1. Explicitly wrap the rhs in a closure as shown. This is quite noisy. 2. Introduce a generalized function-currying syntax that can takeDictionary.init(grouping:by:), bind thebyargument to\.name, and return a single-argument function. This seems unlikely to be added to the language, and a long way off in any case. 3. Implement the|>as a special form in the language, that gives it special behaviour for cases like this. (e.g.|> Dictionary(grouping:by: \.name)). This adds syntactic complexity to the language, and privileges the first argument over the others, which might not always work nicely.In any case, the resultant spelling would still be quite wordy, and less clear than the simple
grouped(by:)andkeyed(by:)methods.Don't pass the
Keytokeyed(by:)'scombineclosureThe proposed
keyed(by:combine:)API takes an optionalcombineclosure with this type:This differs from the
combineclosure expected by the currentDictionary.init(_:uniquingKeysWith:)API, which only passes the old and new element, but not theKey:If the caller needs the
keyin their decision to pick between the old and new value, they would be required to re-compute it for themselves. This looks like a needless artificial restriction: at the point at which thecombineclosure is called, the key is already available, and it could just be provided directly.groupedByandkeyedByThe
by:argument labels are lost when using trailing-closure syntax:Authors are forced to pick between the terseness of closure syntax as shown, or the prose-like clarity of regular closure passing:
There's a fair argument to be made that the best of both worlds would be to move the
by:from the argument label, into the function's base name, allowing for these two spellings:Acknowledgments
None (yet?)