Skip to content

Foundations for BList#825

Draft
loladenney wants to merge 25 commits into
typelevel:l/faster-immutable-list-634from
loladenney:l/faster-immutable-list-634
Draft

Foundations for BList#825
loladenney wants to merge 25 commits into
typelevel:l/faster-immutable-list-634from
loladenney:l/faster-immutable-list-634

Conversation

@loladenney

Copy link
Copy Markdown
Contributor

building out the framework for the new datatype

Comment thread core/src/main/scala/cats/collections/BList.scala Outdated
@johnynek

Copy link
Copy Markdown
Contributor

This is the same issue as #809 and #634 right?

A lot of the same discussion from #809 would be relevant here I think.

loladenney and others added 2 commits May 28, 2026 13:27
Co-authored-by: Sergey Torgashov <satorg@users.noreply.github.com>
Comment thread core/src/main/scala/cats/collections/BList.scala Outdated
Comment thread core/src/main/scala/cats/collections/BList.scala Outdated
Comment thread core/src/main/scala/cats/collections/BList.scala Outdated
Comment thread core/src/main/scala/cats/collections/BList.scala Outdated
loladenney and others added 3 commits June 2, 2026 10:13
Co-authored-by: Sergey Torgashov <satorg@users.noreply.github.com>
Co-authored-by: Sergey Torgashov <satorg@users.noreply.github.com>
}

// (maybe impl will be covariant or not)
private case class Impl[A](offset: Int, block: Array[A], tail: BList[A]) extends NonEmpty[A] {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think you can make this covariant if block: Array[_ <: A] that said, since we don't have a classtag for the type A and we may not want to require it, we may want to instead use Array[Any] and cast when we get out, which would require boxing primitives, but List also boxes primitives.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It used to be Array[Any], but it ended up requiring so many uses of .asInstanceOf, and I didn't like how cluttered and confusing it made everything. Is there a good reason why we would want Impl to be covariant? If its worth it then I will try to find ways to contain the typecasts in a maintainable way.

Comment thread core/src/main/scala/cats/collections/BList.scala Outdated
}
def getUnsafe(idx: Long): A = {
if (idx < 0)
throw new IndexOutOfBoundsException

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

we are throwing a different kind of exception for < 0 vs too large. Does List also do that? I would comment and follow List behavior here so it is fully compliant.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Just checked and it seems to throw the same index out of bounds exception for index too small or too large!

Comment thread core/src/main/scala/cats/collections/BList.scala Outdated
Comment thread core/src/main/scala/cats/collections/BList.scala Outdated
}

def empty[A]: BList[A] = Empty
def unapply[A](l: BList[A]): Option[(A, BList[A])] = l.uncons

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this should be on the object NonEmpty inside BList and we should also have an apply method that builds a NonEmpty by prepending onto a BList. So BList.NonEmpty(h, t) can be used in construction and deconstruction.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yes, makes sense. I think something like this should work:

object BList {
  object NonEmpty {
    def apply[A](h: A, t: BList[A]): NonEmpty[B] = ...
    def unapply[A](l: BList.NonEmpty[A]): Some[(A, BList[A])] = l.uncons
  }
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

What kinds of things do in the object NonEmpty vs the object Impl?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Also, should the return type of unapply still keep the Option?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What kinds of things do in the object NonEmpty vs the object Impl?

NonEmpty is a part of the public API, whereas Impl hosts implementation details which we don't want to reveal.

Also, should the return type of unapply still keep the Option?

That is a part of the trick: if we returned Option[...], then Scala compiler might struggle on exhausiveness checks in some cases – because it doesn't know whether it resolves to Some or None at runtime.

However, when we return Some, then we reassure the compiler that the result does always exist:

val fooBList: BList[Int] = ...

fooBList match {
  case BList.NonEmpty(head, tail) => // only matches if our `fooBList` is a `NonEmpty` instance.
  case BList.Empty =>
  // or `case _ : BList.Empty.type =>` or just `case _ =>` – the `match` should be exhaustive in any case.
}

Minimal-ish example: https://scastie.scala-lang.org/smsfk232TM6C01PJonKThQ

// TODO can put methods in here that are only safe for nonempty (ex. head, reduce)
def head: A
def tail: BList[A]
// def reduce

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'd suggest establishing NonEmpty API here sooner than later. We don't need to repeat everything from BList, but only the methods that change their signatures for NonEmpty, e.g.:

def map[B](fn: A => B): BList.NonEmpty[B]

It would give us a clearer picture of all the requirements and constraints that we'll need for the implementation.

def map[B](fn: A => B): BList[B]
def foldLeft[B](init: B)(fn: (B, A) => B): B
def drop(n: Long): BList[A]
def combineK[B >: A](l2: BList[B]): BList[B]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm not sure about this name – combineK. It is very generic and moreover K suffix suggests higher kinded types. The name comes from SemigroupK, which can be used for many different types, not just collections.

Sequence-like structures usually stick to names like concat or ++. Alternatively, since BList tries to resemble the regular List, we can consider ::: as well.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I can rename it. I think I chose this one because Arman called it that when he gave me an initial list of methods to implement.

Comment thread core/src/main/scala/cats/collections/BList.scala
Comment thread core/src/main/scala/cats/collections/BList.scala
Comment thread core/src/main/scala/cats/collections/BList.scala Outdated
}
def tail: BList[A] = {
if (offset < BlockSize - 1) {
Impl(offset + 1, block, tailBList)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Are we sure we want to share arrays between two BList instances? It can results in discarded objects blocked from GC:

val largeThings = BList(largeThing1, largeThing2, largeThing3)
val res = largeThings.tail

In this case, res won't have largeThing1 becasue it was discarded by .tail, but it will be still referenced by the same array that was shared between largeThings and res. Therefore, even if largeThings collection gets GC-ed, largeThing1 will be kept in memory until res is GC-ed as well.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

the fix will be in the next commit. I also made the same fix for uncons.

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.

4 participants