Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

41 changes: 41 additions & 0 deletions arrow-libs/core/arrow-core/api/android/arrow-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,45 @@ public final class arrow/core/MemoizedDeepRecursiveFunctionKt {
public static synthetic fun MemoizedDeepRecursiveFunction$default (Larrow/core/MemoizationCache;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)Lkotlin/DeepRecursiveFunction;
}

public abstract interface class arrow/core/MonotoneCollectionBuilder {
public abstract fun _add (Ljava/lang/Object;)V
public abstract fun _addAll (Ljava/util/Collection;)V
}

public abstract interface class arrow/core/MonotoneCollectionBuilder$NonEmpty {
}

public final class arrow/core/MonotoneCollectionBuilderKt {
public static final fun add (Larrow/core/MonotoneCollectionBuilder;Ljava/lang/Object;)V
public static final fun addAll (Larrow/core/MonotoneCollectionBuilder;Larrow/core/NonEmptyCollection;)V
public static final fun addAll (Larrow/core/MonotoneCollectionBuilder;Ljava/lang/Iterable;)V
public static final fun addAll (Larrow/core/MonotoneCollectionBuilder;Ljava/util/Collection;)V
public static final fun asNonEmptyList (Ljava/util/List;)Ljava/util/List;
public static final fun asNonEmptySet (Ljava/util/Set;)Ljava/util/Set;
public static final fun buildNonEmptyList (ILkotlin/jvm/functions/Function1;)Ljava/util/List;
public static final fun buildNonEmptyList (Lkotlin/jvm/functions/Function1;)Ljava/util/List;
public static final fun buildNonEmptySet (ILkotlin/jvm/functions/Function1;)Ljava/util/Set;
public static final fun buildNonEmptySet (Lkotlin/jvm/functions/Function1;)Ljava/util/Set;
}

public abstract interface class arrow/core/MonotoneMutableList : arrow/core/MonotoneCollectionBuilder, java/util/List, kotlin/jvm/internal/markers/KMappedMarker {
public static final field Companion Larrow/core/MonotoneMutableList$Companion;
}

public final class arrow/core/MonotoneMutableList$Companion {
public final fun invoke ()Larrow/core/MonotoneMutableList;
public final fun invoke (I)Larrow/core/MonotoneMutableList;
}

public abstract interface class arrow/core/MonotoneMutableSet : arrow/core/MonotoneCollectionBuilder, java/util/Set, kotlin/jvm/internal/markers/KMappedMarker {
public static final field Companion Larrow/core/MonotoneMutableSet$Companion;
}

public final class arrow/core/MonotoneMutableSet$Companion {
public final fun invoke ()Larrow/core/MonotoneMutableSet;
public final fun invoke (I)Larrow/core/MonotoneMutableSet;
}

public abstract interface class arrow/core/NonEmptyCollection : java/util/Collection, kotlin/jvm/internal/markers/KMappedMarker {
public abstract fun distinct-1X0FA-Y ()Ljava/util/List;
public abstract fun distinctBy-0-xjo5U (Lkotlin/jvm/functions/Function1;)Ljava/util/List;
Expand All @@ -371,7 +410,9 @@ public final class arrow/core/NonEmptyCollection$DefaultImpls {
public static fun distinctBy-0-xjo5U (Larrow/core/NonEmptyCollection;Lkotlin/jvm/functions/Function1;)Ljava/util/List;
public static fun firstOrNull (Larrow/core/NonEmptyCollection;)Ljava/lang/Object;
public static fun flatMap-0-xjo5U (Larrow/core/NonEmptyCollection;Lkotlin/jvm/functions/Function1;)Ljava/util/List;
public static fun getHead (Larrow/core/NonEmptyCollection;)Ljava/lang/Object;
public static fun isEmpty (Larrow/core/NonEmptyCollection;)Z
public static fun lastOrNull (Larrow/core/NonEmptyCollection;)Ljava/lang/Object;
public static fun map-0-xjo5U (Larrow/core/NonEmptyCollection;Lkotlin/jvm/functions/Function1;)Ljava/util/List;
public static fun mapIndexed-0-xjo5U (Larrow/core/NonEmptyCollection;Lkotlin/jvm/functions/Function2;)Ljava/util/List;
public static fun toNonEmptyList-1X0FA-Y (Larrow/core/NonEmptyCollection;)Ljava/util/List;
Expand Down
44 changes: 34 additions & 10 deletions arrow-libs/core/arrow-core/api/arrow-core.klib.api

Large diffs are not rendered by default.

41 changes: 41 additions & 0 deletions arrow-libs/core/arrow-core/api/jvm/arrow-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,45 @@ public final class arrow/core/MemoizedDeepRecursiveFunctionKt {
public static synthetic fun MemoizedDeepRecursiveFunction$default (Larrow/core/MemoizationCache;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)Lkotlin/DeepRecursiveFunction;
}

public abstract interface class arrow/core/MonotoneCollectionBuilder {
public abstract fun _add (Ljava/lang/Object;)V
public abstract fun _addAll (Ljava/util/Collection;)V
}

public abstract interface class arrow/core/MonotoneCollectionBuilder$NonEmpty {
}

public final class arrow/core/MonotoneCollectionBuilderKt {
public static final fun add (Larrow/core/MonotoneCollectionBuilder;Ljava/lang/Object;)V
public static final fun addAll (Larrow/core/MonotoneCollectionBuilder;Larrow/core/NonEmptyCollection;)V
public static final fun addAll (Larrow/core/MonotoneCollectionBuilder;Ljava/lang/Iterable;)V
public static final fun addAll (Larrow/core/MonotoneCollectionBuilder;Ljava/util/Collection;)V
public static final fun asNonEmptyList (Ljava/util/List;)Ljava/util/List;
public static final fun asNonEmptySet (Ljava/util/Set;)Ljava/util/Set;
public static final fun buildNonEmptyList (ILkotlin/jvm/functions/Function1;)Ljava/util/List;
public static final fun buildNonEmptyList (Lkotlin/jvm/functions/Function1;)Ljava/util/List;
public static final fun buildNonEmptySet (ILkotlin/jvm/functions/Function1;)Ljava/util/Set;
public static final fun buildNonEmptySet (Lkotlin/jvm/functions/Function1;)Ljava/util/Set;
}

public abstract interface class arrow/core/MonotoneMutableList : arrow/core/MonotoneCollectionBuilder, java/util/List, kotlin/jvm/internal/markers/KMappedMarker {
public static final field Companion Larrow/core/MonotoneMutableList$Companion;
}

public final class arrow/core/MonotoneMutableList$Companion {
public final fun invoke ()Larrow/core/MonotoneMutableList;
public final fun invoke (I)Larrow/core/MonotoneMutableList;
}

public abstract interface class arrow/core/MonotoneMutableSet : arrow/core/MonotoneCollectionBuilder, java/util/Set, kotlin/jvm/internal/markers/KMappedMarker {
public static final field Companion Larrow/core/MonotoneMutableSet$Companion;
}

public final class arrow/core/MonotoneMutableSet$Companion {
public final fun invoke ()Larrow/core/MonotoneMutableSet;
public final fun invoke (I)Larrow/core/MonotoneMutableSet;
}

public abstract interface class arrow/core/NonEmptyCollection : java/util/Collection, kotlin/jvm/internal/markers/KMappedMarker {
public abstract fun distinct-1X0FA-Y ()Ljava/util/List;
public abstract fun distinctBy-0-xjo5U (Lkotlin/jvm/functions/Function1;)Ljava/util/List;
Expand All @@ -371,7 +410,9 @@ public final class arrow/core/NonEmptyCollection$DefaultImpls {
public static fun distinctBy-0-xjo5U (Larrow/core/NonEmptyCollection;Lkotlin/jvm/functions/Function1;)Ljava/util/List;
public static fun firstOrNull (Larrow/core/NonEmptyCollection;)Ljava/lang/Object;
public static fun flatMap-0-xjo5U (Larrow/core/NonEmptyCollection;Lkotlin/jvm/functions/Function1;)Ljava/util/List;
public static fun getHead (Larrow/core/NonEmptyCollection;)Ljava/lang/Object;
public static fun isEmpty (Larrow/core/NonEmptyCollection;)Z
public static fun lastOrNull (Larrow/core/NonEmptyCollection;)Ljava/lang/Object;
public static fun map-0-xjo5U (Larrow/core/NonEmptyCollection;Lkotlin/jvm/functions/Function1;)Ljava/util/List;
public static fun mapIndexed-0-xjo5U (Larrow/core/NonEmptyCollection;Lkotlin/jvm/functions/Function2;)Ljava/util/List;
public static fun toNonEmptyList-1X0FA-Y (Larrow/core/NonEmptyCollection;)Ljava/util/List;
Expand Down
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.
*/
Comment on lines +10 to +13
Copy link
Copy Markdown
Member

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 buildNonEmptyList function should have just a single contract: by the end of the block you should have at least one element (regardless of using add, addAll, remove, or whatever you want). Here you're introducing a completely different notion.

Copy link
Copy Markdown
Collaborator Author

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 throw if the resulting list is non-empty, but that's far from a static guarantee. One can always create intermediate Lists and remove and add to 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 add and addAll, never needing remove.

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 add or addAll(NonEmptyCollection), or your own custom method that has the apt contract (where, of course, you'd be expected to ultimately call add or addAll). The only way to get around this guarantee is through a bad user-defined contract (to which there are compiler checkers coming eventually), or a cast to MonotoneCollectionBuilder.NonEmpty. This is similar to how you can already break NonEmptyList if you cast its all to MutableList, at least on the JVM.

If it helps, MonotoneCollectionBuilder is a suspend-less SequenceScope/FlowCollector. add/addAll correspond to yield/yieldAll and emit/emitAll.

MonotoneCollectionBuilder is 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 the Writer monad. This is incredibly useful because it doesn't force the implementation to use a List, any monoid will do. That's why the Set implementation is so easy, since that's another monoid.
What the buildNonEmptyX functions do is that they let you use any semigroup because they guarantee at least one add or non-empty addAll call. NonEmptyList and NonEmptySet are semigroups.
One can even imagine extending MonotoneCollectionBuilder (which ought to be called Writer or Accumulator) further by adding effect combinators like what we have for Raise. A mapWriter is easily implementable, and serves as a more efficient way to map a 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.

Copy link
Copy Markdown
Collaborator Author

@kyay10 kyay10 Jan 11, 2026

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!

Copy link
Copy Markdown
Member

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 to add or addAll is done. We really made a big effort to reduce Arrow's API to its minimum, and this PR goes into the opposite direction.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

given that at this point we have no way to enforce that at least one call to add or addAll is done

Do you mean that buildNonEmptyList doesn'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 the remove methods), but I think only a fraction of the API is really necessary, and we can always extend it later

@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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,64 @@ package arrow.core
* Common interface for collections that always have
* at least one element (available from [head]).
*/
@SubclassOptInRequired(PotentiallyUnsafeNonEmptyOperation::class)
public interface NonEmptyCollection<out E> : Collection<E> {
override fun isEmpty(): Boolean = false
public val head: E
public val head: E get() = first()

public operator fun plus(element: @UnsafeVariance E): NonEmptyCollection<E>

public operator fun plus(elements: Iterable<@UnsafeVariance E>): NonEmptyCollection<E>

public fun toNonEmptySet(): NonEmptySet<E> = toNonEmptySetOrThrow()
public fun toNonEmptyList(): NonEmptyList<E> = toNonEmptyListOrThrow()
public fun toNonEmptySet(): NonEmptySet<E> = buildNonEmptySet(size) {
addAll(this@NonEmptyCollection)
this
}

public fun toNonEmptyList(): NonEmptyList<E> = buildNonEmptyList(size) {
addAll(this@NonEmptyCollection)
this
}

// These functions take precedence over the extensions in [Collection].
// This way non-emptiness is tracked by the type system.

public fun firstOrNull(): E = head
public fun lastOrNull(): E

public fun distinct(): NonEmptyList<E> =
delegate { it.distinct() }
public fun <K> distinctBy(selector: (E) -> K): NonEmptyList<E> =
delegate { it.distinctBy(selector) }
public fun <T> flatMap(transform: (E) -> NonEmptyCollection<T>): NonEmptyList<T> =
delegate { it.flatMap(transform) }
public fun <T> map(transform: (E) -> T): NonEmptyList<T> =
delegate { it.map(transform) }
public fun <T> mapIndexed(transform: (index:Int, E) -> T): NonEmptyList<T> =
delegate { it.mapIndexed(transform) }
public fun <T> zip(other: NonEmptyCollection<T>): NonEmptyCollection<Pair<E, T>> =
delegate { it.zip(other) }

/**
* Convenience method which delegates the implementation to [Collection],
* and wraps the resulting [List] as a non-empty one.
*/
@OptIn(PotentiallyUnsafeNonEmptyOperation::class)
private inline fun <T> delegate(crossinline f: (Collection<E>) -> List<T>): NonEmptyList<T> =
f(this as Collection<E>).wrapAsNonEmptyListOrThrow()
public fun lastOrNull(): E = last()

public fun distinct(): NonEmptyList<E> = toNonEmptySet().toNonEmptyList()
public fun <K> distinctBy(selector: (E) -> K): NonEmptyList<E> = buildNonEmptyList(size) {
add(head) // head is always distinct
val seen = hashSetOf<K>()
var isFirst = true
for (e in this@NonEmptyCollection) {
if (seen.add(selector(e)) && !isFirst) add(e)
isFirst = false
}
this
}
public fun <T> flatMap(transform: (E) -> NonEmptyCollection<T>): NonEmptyList<T> = buildNonEmptyList(size) {
val iterator = this@NonEmptyCollection.iterator()
do addAll(transform(iterator.next())) while (iterator.hasNext())
this
}
public fun <T> map(transform: (E) -> T): NonEmptyList<T> = buildNonEmptyList(size) {
val iterator = this@NonEmptyCollection.iterator()
do add(transform(iterator.next())) while (iterator.hasNext())
this
}
public fun <T> mapIndexed(transform: (index:Int, E) -> T): NonEmptyList<T> = buildNonEmptyList(size) {
var i = 0
val iterator = this@NonEmptyCollection.iterator()
do add(transform(i++, iterator.next())) while (iterator.hasNext())
this
}
public fun <T> zip(other: NonEmptyCollection<T>): NonEmptyCollection<Pair<E, T>> = buildNonEmptyList(minOf(size, other.size)) {
val first = this@NonEmptyCollection.iterator()
val second = other.iterator()
do add(first.next() to second.next()) while (first.hasNext() && second.hasNext())
this
}
}

/**
Expand All @@ -48,5 +70,5 @@ public interface NonEmptyCollection<out E> : Collection<E> {
*/
@RequiresOptIn
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.FUNCTION)
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CONSTRUCTOR)
public annotation class PotentiallyUnsafeNonEmptyOperation
Loading
Loading