Skip to content
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

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open

Add initializers #24

wants to merge 7 commits into from

Conversation

ladvoc
Copy link
Owner

@ladvoc ladvoc commented Sep 14, 2024

This PR adds two new initializers: init(uniquingWith:) and init(discardingConflicts:). Closes #19.

@ladvoc
Copy link
Owner Author

ladvoc commented Sep 14, 2024

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.

@ladvoc ladvoc requested a review from DandyLyons September 14, 2024 20:34
@DandyLyons
Copy link
Collaborator

Here's my feedback for init(discardConflicting:): dc62d25

/// - 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
Copy link
Collaborator

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.

Copy link
Owner Author

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

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.

https://swiftpackageindex.com/pointfreeco/swift-identified-collections/main/documentation/identifiedcollections/identifiedarray/init(_:uniquingidswith:)

/// pair will be excluded from the new dictionary.
@inlinable public init<S>(
_ pairs: S,
uniquingWith combine: (Element, Conflict) throws -> Element
Copy link
Collaborator

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.

Copy link
Collaborator

@DandyLyons DandyLyons Sep 14, 2024

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() {
Copy link
Collaborator

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.

Copy link
Owner Author

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:

  1. Returns the value associated with the existing key $k$.
  2. Returns the value associated with the new occurrence of key $k$.
  3. 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 $(a, b)$. Next, it encounters $(a, c)$ (which conflicts because of a duplicate left value). In the closure, it is possible to return $(z, b)$; this fixes the conflicting left value but creates a new conflict with the right value. This condition is not possible with Dictionary since no matter what value is returned, the conflict will be resolved.

Please let me know if this makes sense.

@DandyLyons
Copy link
Collaborator

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.

@DandyLyons
Copy link
Collaborator

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

@ladvoc
Copy link
Owner Author

ladvoc commented Sep 15, 2024

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

@DandyLyons
Copy link
Collaborator

Not conflicting but related, I made a PR for another possible approach to fixing the uniquing problem: #26

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add initializer with uniquing closure(s)
2 participants