-
Notifications
You must be signed in to change notification settings - Fork 468
NonEmptyCollection builders #3837
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
Open
kyay10
wants to merge
16
commits into
main
Choose a base branch
from
kyay10/non-empty-mutable-collections
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
76e0ee4
Add NonEmptyCollection builders
kyay10 f0159ef
Add missing zip overload and loosen `asNonEmptyList/Set` where clause
kyay10 01162c6
Remove unused functions and fix MonotoneX equality/hashCode
kyay10 966fddb
Fix MonotoneMutableSet.Impl equals/hashCode
kyay10 9294c0e
Fi inlined bytecode issue
kyay10 4c808a8
Override lambda-taking methods in NonEmptySet
kyay10 882dfb0
Extract into separate file
kyay10 0d7ad8c
Mild formatting changes
kyay10 b484c79
Auto-update API files
kyay10 be1045d
Provide defaults for `plus`
kyay10 c76c580
Remove redundant opt-ins
kyay10 e608435
Remove usage of 2.3 contracts, and change to effect-y API
kyay10 9ae716f
Remove NonEmptyCollection.plus default impls and NonEmptySet overrides
kyay10 7cbfa3b
Bring back Nes overrides due to KT-80101
kyay10 e98ea13
Auto-update API files
kyay10 9e92636
Reduce API surface
kyay10 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
245 changes: 232 additions & 13 deletions
245
arrow-libs/core/arrow-core-high-arity/src/commonMain/kotlin/arrow/core/NonEmptyList.kt
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
128 changes: 128 additions & 0 deletions
128
arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/MonotoneCollectionBuilder.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| @file:Suppress("API_NOT_AVAILABLE") | ||
| @file:OptIn(ExperimentalContracts::class) | ||
| package arrow.core | ||
|
|
||
| import arrow.core.MonotoneCollectionBuilder.NonEmpty | ||
| import kotlin.contracts.ExperimentalContracts | ||
| import kotlin.contracts.InvocationKind | ||
| import kotlin.contracts.contract | ||
|
|
||
| /** | ||
| * A builder for collections that can only grow by adding elements. | ||
| * Removing elements is not supported to preserve monotonicity. | ||
| */ | ||
| @SubclassOptInRequired(PotentiallyUnsafeNonEmptyOperation::class) | ||
| public interface MonotoneCollectionBuilder<in E> { | ||
| // underscored so that addAll(Nec<E>) extension overload takes precedence | ||
| // Turning this into an abstract class doesn't work well because of KT-83602 | ||
| public fun _addAll(elements: Collection<E>) | ||
|
|
||
| public fun _add(element: E) | ||
|
|
||
| /** | ||
| * Marker interface for builders that guarantee at least one element has been added. | ||
| * @see MonotoneCollectionBuilder | ||
| * @see buildNonEmptyList | ||
| */ | ||
| public interface NonEmpty | ||
| } | ||
|
|
||
| public fun <E> MonotoneCollectionBuilder<E>.add(element: E) { | ||
| contract { returns() implies (this@add is NonEmpty) } | ||
| return _add(element) | ||
| } | ||
|
|
||
| public fun <E> MonotoneCollectionBuilder<E>.addAll(elements: Iterable<E>): Unit = when (elements) { | ||
| is Collection -> addAll(elements) | ||
| else -> for (item in elements) add(item) | ||
| } | ||
|
|
||
| public fun <E> MonotoneCollectionBuilder<E>.addAll(elements: NonEmptyCollection<E>) { | ||
| contract { returns() implies (this@addAll is NonEmpty) } | ||
| _addAll(elements) | ||
| } | ||
|
|
||
| public fun <E> MonotoneCollectionBuilder<E>.addAll(elements: Collection<E>): Unit = _addAll(elements) | ||
|
|
||
| @SubclassOptInRequired(PotentiallyUnsafeNonEmptyOperation::class) | ||
| private abstract class MonotoneMutableCollectionImpl<E>(private val underlying: MutableCollection<E>) : MonotoneCollectionBuilder<E>, NonEmpty { | ||
| final override fun _addAll(elements: Collection<E>) { underlying.addAll(elements) } | ||
| final override fun _add(element: E) { underlying.add(element) } | ||
|
|
||
| final override fun equals(other: Any?) = underlying == other | ||
| final override fun hashCode() = underlying.hashCode() | ||
| final override fun toString() = underlying.toString() | ||
| } | ||
|
|
||
| /** | ||
| * A mutable list that can only grow by adding elements. | ||
| * Removing elements is not supported to preserve monotonicity. | ||
| */ | ||
| @SubclassOptInRequired(PotentiallyUnsafeNonEmptyOperation::class) | ||
| public interface MonotoneMutableList<E>: MonotoneCollectionBuilder<E>, List<E> { | ||
| public companion object { | ||
| public operator fun <E> invoke(): MonotoneMutableList<E> = Impl() | ||
|
|
||
| public operator fun <E> invoke(initialCapacity: Int): MonotoneMutableList<E> = Impl(initialCapacity) | ||
| } | ||
|
|
||
| @OptIn(PotentiallyUnsafeNonEmptyOperation::class) | ||
| private class Impl<E> private constructor(underlying: MutableList<E>) : MonotoneMutableCollectionImpl<E>(underlying), MonotoneMutableList<E>, List<E> by underlying { | ||
| constructor() : this(ArrayList()) | ||
| constructor(initialCapacity: Int) : this(ArrayList(initialCapacity)) | ||
| } | ||
| } | ||
|
|
||
| @OptIn(PotentiallyUnsafeNonEmptyOperation::class) | ||
| public fun <E, L> L.asNonEmptyList(): NonEmptyList<E> where L : List<E>, L : NonEmpty = NonEmptyList(this) | ||
|
|
||
| public inline fun <E, L> buildNonEmptyList( | ||
| builderAction: MonotoneMutableList<E>.() -> L | ||
| ): NonEmptyList<E> where L : List<E>, L : NonEmpty { | ||
| contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) } | ||
| return builderAction(MonotoneMutableList()).asNonEmptyList() | ||
| } | ||
|
|
||
| public inline fun <E, L> buildNonEmptyList( | ||
| capacity: Int, | ||
| builderAction: MonotoneMutableList<E>.() -> L | ||
| ): NonEmptyList<E> where L : List<E>, L : NonEmpty { | ||
| contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) } | ||
| return builderAction(MonotoneMutableList(capacity)).asNonEmptyList() | ||
| } | ||
|
|
||
| /** | ||
| * A mutable list that can only grow by adding elements. | ||
| * Removing elements is not supported to preserve monotonicity. | ||
| */ | ||
| @SubclassOptInRequired(PotentiallyUnsafeNonEmptyOperation::class) | ||
| public interface MonotoneMutableSet<E>: MonotoneCollectionBuilder<E>, Set<E> { | ||
| public companion object { | ||
| public operator fun <E> invoke(): MonotoneMutableSet<E> = Impl() | ||
| public operator fun <E> invoke(initialCapacity: Int): MonotoneMutableSet<E> = Impl(initialCapacity) | ||
| } | ||
|
|
||
| @OptIn(PotentiallyUnsafeNonEmptyOperation::class) | ||
| private class Impl<E> private constructor(underlying: MutableSet<E>) : MonotoneMutableCollectionImpl<E>(underlying), MonotoneMutableSet<E>, NonEmpty, Set<E> by underlying { | ||
| constructor() : this(LinkedHashSet()) | ||
| constructor(initialCapacity: Int) : this(LinkedHashSet(initialCapacity)) | ||
| } | ||
| } | ||
|
|
||
| @OptIn(PotentiallyUnsafeNonEmptyOperation::class) | ||
| public fun <E, S> S.asNonEmptySet(): NonEmptySet<E> where S : Set<E>, S : NonEmpty = NonEmptySet(this) | ||
|
|
||
| public inline fun <E, S> buildNonEmptySet( | ||
| builderAction: MonotoneMutableSet<E>.() -> S | ||
| ): NonEmptySet<E> where S : Set<E>, S : NonEmpty { | ||
| contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) } | ||
| return builderAction(MonotoneMutableSet()).asNonEmptySet() | ||
| } | ||
|
|
||
| public inline fun <E, S> buildNonEmptySet( | ||
| capacity: Int, | ||
| builderAction: MonotoneMutableSet<E>.() -> S | ||
| ): NonEmptySet<E> where S : Set<E>, S : NonEmpty { | ||
| contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) } | ||
| return builderAction(MonotoneMutableSet(capacity)).asNonEmptySet() | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
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.
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 this is required at all. A
buildNonEmptyListfunction should have just a single contract: by the end of the block you should have at least one element (regardless of usingadd,addAll,remove, or whatever you want). Here you're introducing a completely different notion.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.
Practically, I don't see how it can be done any other way while offering a static guarantee that the list is non-empty. Sure, you can
throwif the resulting list is non-empty, but that's far from a static guarantee. One can always create intermediateLists andremoveandaddto them, as long as they always have a call that guarantees the non-emptiness of the builder.I think it's interesting to see that the Arrow functions affected got by just fine with
addandaddAll, never needingremove.Just to make sure I'm fully clear, this API guarantees that your list is non-empty. The compiler will complain if you don't have an
addoraddAll(NonEmptyCollection), or your own custom method that has the apt contract (where, of course, you'd be expected to ultimately calladdoraddAll). The only way to get around this guarantee is through a bad user-definedcontract(to which there are compiler checkers coming eventually), or a cast toMonotoneCollectionBuilder.NonEmpty. This is similar to how you can already breakNonEmptyListif you cast itsalltoMutableList, at least on the JVM.If it helps,
MonotoneCollectionBuilderis asuspend-lessSequenceScope/FlowCollector.add/addAllcorrespond toyield/yieldAllandemit/emitAll.MonotoneCollectionBuilderis designed as an effect. It's basically a write-only accumulator (maybe we should name it as such). It doesn't know, or care, how the data is stored, just that it's written somewhere. In fact, it's exactly the effect version of theWritermonad. This is incredibly useful because it doesn't force the implementation to use aList, any monoid will do. That's why theSetimplementation is so easy, since that's another monoid.What the
buildNonEmptyXfunctions do is that they let you use any semigroup because they guarantee at least oneaddor non-emptyaddAllcall.NonEmptyListandNonEmptySetare semigroups.One can even imagine extending
MonotoneCollectionBuilder(which ought to be calledWriterorAccumulator) further by adding effect combinators like what we have forRaise. AmapWriteris easily implementable, and serves as a more efficient way tomapa List.Some caffienated handwaving about connections to monoids
There's a deeper connection here in the form of `MonotoneCollectionBuilder.() -> Unit` actually being kind of a free monoid over `T`, and hence isomorphic to `List`. (Technically, it's actually isomorphic to `() -> List` because of exceptions and non-termination and such, and you can add `suspend` modifiers to both, too, with the isomorphism staying). Handwaving a bit, sequencing of `MonotoneCollectionBuilder.() -> Unit` functions corresponds exactly to the monoid's `combine`, with `val id: MonotoneCollectionBuilder.() -> Unit = { }` serving as the identity for the monoid. If you've ever seen how a monoid is modelled in CT, it's through a bunch of morphisms. In some way, `MonotoneCollectionBuilder` lets you map any `T` to a morphism, with the morphism composition being the sequencing. `MonotoneCollectionBuilder.() -> Unit` is the "mother of all monoids", while `List` is the free monoid, analogously to (multi-shot) `Continuation` being the "mother of all monads", while `Free` is the free monad. There's something about "initial" vs "final" encoding here that I don't know enough to comment about. What I do konw is that `MonotoneCollectionBuilder.() -> Unit` is more performant than a `List` because it doesn't involve intermediate lists, in a similar way to how continuations are more performant than `Free` because it doesn't involve intermediate values.Uh oh!
There was an error while loading. Please reload this page.
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 it helps, here's a very simple version of the API to play around with (playground). Let me know if you find any way to break 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.
The question here for me is whether providing this API is useful enough to include it in
arrow-core, given that at this point we have no way to enforce that at least one call toaddoraddAllis done. We really made a big effort to reduce Arrow's API to its minimum, and this PR goes into the opposite direction.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.
Do you mean that
buildNonEmptyListdoesn't enforce that? Because it does! See the playground link above. It's all compile-time checked, relying on contracts and a lambda with an intersection return type.I can have a go at reducing the API surface here if you'd like. I was aiming to have an API similar to
MutableList/Set(without theremovemethods), but I think only a fraction of the API is really necessary, and we can always extend it later