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

Implement basic NonEmptySet #2899

Merged
merged 10 commits into from
Feb 2, 2023
Merged

Conversation

ibado
Copy link
Contributor

@ibado ibado commented Jan 22, 2023

This is a super basic implementation for #2565. After review, if this is fine, I can add any other functionality may be required.

@ibado ibado changed the title Implement basic NonEmptySet Implement basic NonEmptySet #2565 Jan 22, 2023
@ibado ibado changed the title Implement basic NonEmptySet #2565 Implement basic NonEmptySet Jan 22, 2023
Copy link
Member

@serras serras left a comment

Choose a reason for hiding this comment

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

Nice job! I think this just need a few more tests to ensure that the implementation is correct even if somebody else changes or tries to optimize it even further.

@ibado ibado requested a review from serras January 23, 2023 20:51
@serras
Copy link
Member

serras commented Jan 24, 2023

@ibado I'm wondering whether the first + rest separation is really needed apart from the constructor. More concretely, I'm proposing to make NonEmptySet simply a wrapper for Set, but only expose the right constructor:

@JvmInline public value class NonEmptySet<out T> private constructor(
  val elements: Set<T>
): AbstractSet<T> {
  public constructor(first: T, varargs rest: T): this(setOf(first) + rest.toSet())
}

You can still overload the plus operator to return NonEmptySet, but you already know that's the case if one of the arguments is NonEmptySet.

  public operator fun plus(s: Set<@UnsafeVariance T>): NonEmptySet<T> =
    NonEmptySet(elements + s)

This has the added benefit of completely removing the wrapper at runtime, since it's a value class.

@roomscape what do you think?

@roomscape
Copy link
Contributor

@roomscape what do you think?

I think you're right, the first/rest separation may not be needed outside of the constructor. An efficient implementation will always need to create a Set to implement the required operations. Including the first item alongside the others in that set makes the implementation simpler, and I can't actually think of any functionality that would benefit from keeping one item separate.

In that case, your suggestion of a simple wrapper class works very well. Interface delegation would be a good fit:

class NonEmptySet<out T> private constructor(elements: Set<T>): Set<T> by elements

I like the idea of making it an inline class too, but I'm cautious about the implications, and I think the benefits would only be slight. Value classes can use interface delegation, so that would work. But value classes do seem to have some quirks, and in particular I think generic parameters are still experimental. It might be safer to leave that for a future enhancement.

@ibado ibado force-pushed the feature/non-empty-set branch from 2b0dc4b to 5da450b Compare January 28, 2023 12:50
@ibado
Copy link
Contributor Author

ibado commented Jan 28, 2023

I uploaded a simplify version following the approach of @roomscape since I think is nice to delegate the "set" capabilities to the wrapped set. Not sure about the inline value class.. It's up to you.
Please take a look and LMK 🙂
P.S: the overrides for plus are necessary for returning back a NonEmptySet

@ibado ibado force-pushed the feature/non-empty-set branch from 5da450b to 791e86b Compare January 28, 2023 13:05
@ibado ibado force-pushed the feature/non-empty-set branch from 791e86b to 865393d Compare January 28, 2023 18:58
@serras
Copy link
Member

serras commented Jan 30, 2023

@ibado I've been working on a similar PR for NonEmptyList, and I've realized that you need to override equals if you want NonEmptySet to be comparable with Set. This commit shows how.

Once you do this, you can really change to a value class, which removes any wrapper at runtime:

@JvmInline public value class NonEmptySet<out T> private constructor(
  private val elements: Set<T>
) : Set<T> by elements {

@ibado
Copy link
Contributor Author

ibado commented Jan 30, 2023

@serras Done! the equals was already there, but I was not checking for NonEmptySet.
Still not sure how stable value classes are but what a hell 🤣

@ibado
Copy link
Contributor Author

ibado commented Jan 30, 2023

Now :arrow-core:compileTestKotlinJvm is failing 🙃 Looks that also your PR. Something about IR

@serras
Copy link
Member

serras commented Jan 30, 2023

@ibado I think the commit I've just pushed should fix the problem (it's just a different way to write the generator)

@ibado ibado requested review from roomscape and removed request for roomscape January 31, 2023 07:41
@ibado ibado requested review from hoc081098 and roomscape and removed request for hoc081098 and roomscape January 31, 2023 07:41
@serras serras added the 1.2.0 Tickets belonging to 1.1.2 label Jan 31, 2023
Copy link
Member

@nomisRev nomisRev left a comment

Choose a reason for hiding this comment

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

Looks great @ibado! Thank you and congrats on your first contribution 🙌 🥳

Small styling nit.

I think this just need a few more tests to ensure that the implementation is correct

This would also be nice, you can run ./gradlew koverHtmlReport to check the coverage of NonEmptySet.

@serras serras merged commit 118d5dc into arrow-kt:main Feb 2, 2023
@serras
Copy link
Member

serras commented Feb 2, 2023

@ibado congrats on your first contribution! Thanks for your work here, and for ensuring the best quality for this new implementation :)

@ibado
Copy link
Contributor Author

ibado commented Feb 2, 2023

Thanks! Happy to help 😄

@nomisRev nomisRev mentioned this pull request Feb 8, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
1.2.0 Tickets belonging to 1.1.2
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants