-
Notifications
You must be signed in to change notification settings - Fork 1
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
Add initializers #24
base: main
Are you sure you want to change the base?
Add initializers #24
Conversation
The CI workflow for Swift 6 failed to run since Xcode 16 is no longer in beta. I have implemented a quick fix for this. |
Here's my feedback for |
Sources/BijectiveDictionary/BijectiveDictionary+Initializers.swift
Outdated
Show resolved
Hide resolved
/// - pairs: A sequence of left-right pairs to use for the new dictionary. | ||
/// - combine: A closure that is called when a conflict is encountered, receiving the left-right pair | ||
/// and a description of the conflict that exists. | ||
/// - Note: If the combine closure does not resolve the conflict for a given left-right pair, then that left-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.
This API appears to behave slightly differently than the init from Dictionary.
In the Dictionary version combine
will always resolve the conflict. Two values are input, and only one value is returned.
Our initializer is not quite equivalent to the Dictionary API. Our combine
only accepts one Element, not two. In our version, we are responsible for resolving the conflict, whereas, in the Dictionary API, the user is responsible for resolving the conflict.
If the API is designed to mimic Dictionary
's API, and if I see init(uniquingWith:)
, as a user, I would expect to responsible for resolving the conflict (since that's how it works in Dictionary). In other words, I wouldn't expect it to possible for the combine closure to not resolve the conflict.
It's possible that our data structure has a problem space that is different enough that we can't use the same approach as Dictionary here (I'm not sure). But if we need to use a different approach, then we should make the API different enough to the user as well.
I need to think more about the best approach.
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 definitely something to consider.
I think the fundamental issue is that this initializer has to inform the user of 4 potential types of conflicts (as opposed to just conflicting keys with Dictionary
) and allow them to be resolved. For this reason, I think the API will have to differ for this initializer, although, I am not yet sure if this is best approach.
I think it might be helpful to consider some of the ways in which this initializer will be used in real-life to ensure we take the right approach; I will try and come up with some examples.
let pairs = [("A", 1), ("B", 2), ("C", 3)] | ||
var conflicts = [BijectiveDictionary<String, Int>.Conflict]() | ||
|
||
let dict = BijectiveDictionary(pairs) { pair, conflict in |
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.
Because we're using a trailing closure here, the second parameter is not visible here. It's not clear what the purpose of the trailing closure is here.
Perhaps the first parameter should have a required label at the call site to make this more clear.
Then again, perhaps this is nitpicky and unimportant.
IdentifiedCollections has a nearly identical initializer, and they don't seem to mind.
/// pair will be excluded from the new dictionary. | ||
@inlinable public init<S>( | ||
_ pairs: S, | ||
uniquingWith combine: (Element, Conflict) throws -> Element |
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 thought the shape of combine
was going to be (Element, Element) throws -> Element
just like Dictionary.
I think that this could solve a lot of problems. It would be much closer to the Dictionary API. We would no longer be responsible for resolving the conflict.
In fact, the shape of combine
should probably be (Element, Element, Conflict) throws -> Element
Then the user could switch
on the Conflict
and they could decide how to resolve it.
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.
After thinking some more, I think this approach could be good, but it still wouldn't guarantee that the conflict is resolved. The user would hand us an Element
back, but if they hand back a conflicting Element
then we still have to decide what to do.
Maybe we could define a Resolution
type that the user could use to explicitly tell us how they want us to resolve the conflict. Then there's no ambiguity and we simply follow their directions.
Then the shape of combine
could be (Element, Element, Conflict) throws -> (Element, Resolution)
. I'm not a fan of how complicated that closure shape would be, but I think it would solve our problems.
} | ||
|
||
@Test | ||
func uniquingWithNoResolve() { |
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 we shifted the responsibility of resolving the conflict to the user, then we would no longer need to test for this. Which would be nice.
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 see what you are saying. However, as long as the closure returns a left-right pair, there is the possibility that the closure produces a pair that still has a conflict. In Dictionary
, the closure just returns what value to use when there are two conflicting keys; there are three possibilities:
- Returns the value associated with the existing key
$k$ . - Returns the value associated with the new occurrence of key
$k$ . - Returns an arbitrary value to associate with key
$k$ .
Now I will consider an example with BijectiveDictionary
where the user is able to get around resolving the conflict. For instance, say a BijectiveDictionary
is being constructed using this initializer. It is mid way through processing the sequence of left-right pairs and already contains Dictionary
since no matter what value is returned, the conflict will be resolved.
Please let me know if this makes sense.
I suspect that the uniquing problem is more complex than I realize, and that there is a consideration that I'm unaware of. Please let me know if there's something that I'm missing. |
Sorry, this review had a lot of feedback. Hope it's not overwhelming. The tl;dr; is I suspect that if we shift the responsibility of resolving conflict to the user, then we can probably eliminate a lot of the complexity elsewhere. This conversation is probably a good starting point for the review: https://github.com/ladvoc/BijectiveDictionary/pull/24/files#r1759825272 |
@DandyLyons. Not a problem at all. It is best to get it right the first time—especially for public API. I am reviewing your feedback now. |
Not conflicting but related, I made a PR for another possible approach to fixing the uniquing problem: #26 |
This PR adds two new initializers:
init(uniquingWith:)
andinit(discardingConflicts:)
. Closes #19.