Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
116687a
foundations for BList
loladenney May 28, 2026
f8b1a16
Update core/src/main/scala/cats/collections/BList.scala
loladenney May 28, 2026
877d3d6
some more methods
loladenney May 29, 2026
9172ebe
Update core/src/main/scala/cats/collections/BList.scala
loladenney Jun 2, 2026
7224246
Update core/src/main/scala/cats/collections/BList.scala
loladenney Jun 2, 2026
971943b
fixes to drop and toList
loladenney Jun 2, 2026
a1a5d9a
removed explicit typecasts and changed tail to tailBList
loladenney Jun 4, 2026
f9e6c8b
made nonempty object, and other changes addressing comments
loladenney Jun 4, 2026
089d854
starting on test suite with individual tests
loladenney Jun 5, 2026
7888e92
addressing Sergey's commments from june 4th
loladenney Jun 5, 2026
e12726d
commit to check in on CI
loladenney Jun 8, 2026
2a7df73
more individual tests
loladenney Jun 8, 2026
8b02e8a
cleaned up some Impl methods
loladenney Jun 10, 2026
bdf9d36
forgot to format on previous commit
loladenney Jun 10, 2026
d9a0dee
fixed concat and lastOption
loladenney Jun 10, 2026
69865f6
Update core/src/main/scala/cats/collections/BList.scala
loladenney Jun 11, 2026
2c30668
property tests
loladenney Jun 11, 2026
dba9c9a
trying to get past ci again
loladenney Jun 11, 2026
34a91f2
banging my head against the ci wall
loladenney Jun 11, 2026
62c530e
stack overflow in blist generation for property tests addressed
loladenney Jun 11, 2026
d67a55c
fixed uncons and tail to prevent blocking GC
loladenney Jun 12, 2026
f8a1d26
make Impl class, removed case
loladenney Jun 12, 2026
2e56cfb
overrode equals and hashcode
loladenney Jun 15, 2026
d623b83
fixed for scala 3
loladenney Jun 15, 2026
aefdb7b
fixed equals
loladenney Jun 15, 2026
db06cc4
made another version of equals
loladenney Jun 18, 2026
ffde5b5
forgot to run formatter
loladenney Jun 18, 2026
0bf49bd
changed hashcode to match other scala list-like datastructures
loladenney Jun 19, 2026
f18b0e5
removed usedless cogen param
loladenney Jun 23, 2026
cb70945
fixing issues with toList caused by mutable builder
loladenney Jun 24, 2026
ce76352
Revert "removed usedless cogen param"
loladenney Jun 25, 2026
0c35b05
removed usedless cogen param
loladenney Jun 23, 2026
8659fea
Revert "fixing issues with toList caused by mutable builder"
loladenney Jun 25, 2026
7325bf8
iterator, and using it for hashcode and equals
loladenney Jun 25, 2026
62bee70
Update core/src/main/scala/cats/collections/BList.scala
loladenney Jun 29, 2026
12d460b
changes to iterator
loladenney Jun 29, 2026
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
383 changes: 383 additions & 0 deletions core/src/main/scala/cats/collections/BList.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,383 @@
/*
* Copyright (c) 2015 Typelevel
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

package cats.collections

import scala.annotation.tailrec
import scala.annotation.unchecked.uncheckedVariance
import scala.util.hashing.MurmurHash3

sealed abstract class BList[+A] {
def uncons: Option[(A, BList[A])]
def prepend[B >: A](a: B): BList.NonEmpty[B]
def headOption: Option[A]
def tailOption: Option[BList[A]]
def get(idx: Long): Option[A]
def getUnsafe(idx: Long): A
Comment thread
satorg marked this conversation as resolved.
def lastOption: Option[A]
def size: Long
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 concat[B >: A](l2: BList[B]): BList[B]
def toList: List[A]
def isEmpty: Boolean
def iterator: Iterator[A]

final def ::[B >: A](a: B): BList[B] = prepend(a)
final def ++[B >: A](l2: BList[B]): BList[B] = concat(l2)

// for development and testing
private[collections] def toStringInBlocks: String

override def toString: String = {
val strb = new java.lang.StringBuilder
strb.append("BList(")
@tailrec
def loop(first: Boolean, l: BList[A]): Unit =
l.uncons match {
case None => ()
case Some((h, t)) =>
if (!first) strb.append(", "): Unit
strb.append(h.toString)
loop(false, t)
}

loop(true, this)
strb.append(")")
strb.toString
}

def equals(other: Any): Boolean
def hashCode(): Int
}

object BList {
final private[collections] val BlockSize = 4 // test with different values

case object Empty extends BList[Nothing] {
def uncons = None
def prepend[B >: Nothing](a: B): BList.NonEmpty[B] = {
val ary = new Array[Any](BlockSize)
val offset = BlockSize - 1
ary(offset) = a
Impl(offset, ary, Empty)
}
def headOption: None.type = None
def tailOption: None.type = None
def get(idx: Long): None.type = None
def getUnsafe(idx: Long): Nothing = throw new IndexOutOfBoundsException
def lastOption: None.type = None
def size: Long = 0
def map[B](fn: Nothing => B): BList[B] = Empty
def foldLeft[B](acc: B)(fn: (B, Nothing) => B): B = acc
def drop(n: Long): BList[Nothing] = Empty
def concat[B](l2: BList[B]): BList[B] = l2
override def toList: List[Nothing] = Nil
def isEmpty: Boolean = true
def iterator: Iterator[Nothing] = Iterator.empty
private[collections] def toStringInBlocks: String = "Empty"

override def equals(other: Any): Boolean = other match {
case _: Empty.type => true
case _ => false
}

override def hashCode(): Int = Empty.iterator.##

}
sealed abstract class NonEmpty[+A] extends BList[A] {
def head: A
def tail: BList[A]
def map[B](fn: A => B): BList.NonEmpty[B]
def concat[B >: A](l2: BList[B]): BList.NonEmpty[B]
def uncons: Some[(A, BList[A])]
def headOption: Some[A]
def tailOption: Some[BList[A]]
}

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

private object Impl {
def apply[A](offset: Int, block: Array[A], tailBList: BList[A]): Impl[A] =
new Impl(offset, block, tailBList)
def apply[A](offset: Int, block: Array[Any], tailBList: BList[A]): Impl[A] =
new Impl(offset, block.asInstanceOf[Array[A]], tailBList)
def unapply[A](l: Impl[A]): Some[(Int, Array[A], BList[A])] =
Some((l.offset, l.block, l.tailBList))
}

private class Impl[+A](val offset: Int, val block: Array[A @uncheckedVariance], val tailBList: BList[A])
extends NonEmpty[A] {

override def equals(other: Any): Boolean =
other match {
case o: Impl[_] =>
this.iterator.sameElements(o.iterator.asInstanceOf[Iterator[A]])
case _ => false
}

override def hashCode: Int =
MurmurHash3.orderedHash(this.iterator)

def uncons: Some[(A, BList[A])] = {
Some((block(offset), this.tail))
}
def prepend[B >: A](a: B): BList.NonEmpty[B] = {
if (offset > 0) {
val ary = block.clone().asInstanceOf[Array[B]]
val nextOffset = offset - 1
ary(nextOffset) = a
Impl(nextOffset, ary, tailBList)
} else {
val ary = new Array[Any](BlockSize)
val offset = BlockSize - 1
ary(offset) = a
Impl(offset, ary, this)
}
}
def head: A = {
block(offset)
}
def tail: BList[A] = {
if (offset < BlockSize - 1) {
val ary = new Array[Any](BlockSize)
System.arraycopy(block, offset + 1, ary, offset + 1, BlockSize - (offset + 1))
Impl(offset + 1, ary, tailBList)
} else {
tailBList
}
}
def headOption: Some[A] = {
Some(block(offset))
}
def tailOption: Some[BList[A]] = {
Some(tail)
}
def get(idx: Long): Option[A] = {
if (idx < 0) { None }
else {
@tailrec
def go(idx: Long, l: BList[A]): Option[A] = {
l match {
case Empty => None
case impl: Impl[A] @unchecked =>
if (idx < BlockSize - impl.offset) {
Some(impl.block(impl.offset + idx.toInt))
} else {
go(idx - (BlockSize - impl.offset), impl.tailBList)
}
}
}
go(idx, this)
}
}
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!


@tailrec
def go(idx: Long, l: BList[A]): A = {
l match {
case Empty => throw new IndexOutOfBoundsException
case impl: Impl[A] @unchecked =>
if (idx < BlockSize - impl.offset) {
impl.block(impl.offset + idx.toInt)
} else {
go(idx - (BlockSize - impl.offset), impl.tailBList)
}
}
}
go(idx, this)
}
def lastOption: Some[A] = {
@tailrec
def go(self: Impl[A]): Some[A] = {
self.tailBList match {
case Empty => Some(self.block(BlockSize - 1))
case next: Impl[A] @unchecked => go(next)
}
}
go(this)
}
Comment thread
satorg marked this conversation as resolved.
def size: Long = {
@tailrec
def loop(l: BList[A], acc: Long): Long = {
l match {
case Empty => acc
case impl: Impl[A] @unchecked => loop(impl.tailBList, acc + (BlockSize - impl.offset))
}
}
loop(this, 0L)
}
def map[B](fn: A => B): BList.NonEmpty[B] = {
val ary = new Array[Any](BlockSize)
var i = offset
while (i < ary.length) {
ary(i) = fn(block(i))
i += 1
}
Impl(offset, ary, tailBList.map(fn))
}
def foldLeft[B](acc: B)(fn: (B, A) => B): B = {
@tailrec
def loop(acc: B, l: BList[A]): B =
l match {
case Empty => acc
case impl: Impl[A] @unchecked =>
var newacc = acc
var i = impl.offset
while (i < impl.block.length) {
newacc = fn(newacc, impl.block(i))
i += 1
}
loop(newacc, impl.tailBList)
}
loop(acc, this)
}
def drop(n: Long): BList[A] = {
@tailrec
def go(n: Long, l: BList[A]): BList[A] = {
if (n <= 0) {
l
} else {
l match {
case Empty =>
Empty
case impl: Impl[A] @unchecked =>
if (n >= BlockSize - impl.offset) {
go(n - (BlockSize - impl.offset), impl.tailBList)
} else {
val ary = new Array[Any](BlockSize)
val newOffset: Int = impl.offset + n.asInstanceOf[Int] // type conversion safe because 0<n<BlockSize
System.arraycopy(impl.block, newOffset, ary, newOffset, BlockSize - newOffset)
Impl(newOffset, ary, impl.tailBList)
}

}
}
}
go(n, this)
}
def concat[B >: A](l2: BList[B]): BList.NonEmpty[B] = {
// not tail rec
def go(self: Impl[A]): BList.NonEmpty[B] = {
self.tailBList match {
case Empty => Impl(self.offset, self.block.asInstanceOf[Array[B]], l2)
case next: Impl[A] @unchecked => Impl(self.offset, self.block.asInstanceOf[Array[B]], go(next))
}
}

// for now, the only optimization is checking if either are empty
(this, l2) match {
case (_, Empty) => this
case (_, _) => go(this)
}
}

override def toList: List[A] = {
val builder = List.newBuilder[A]
@tailrec
def loop(l: BList[A]): List[A] =
l match {
case Empty => builder.result()
case impl: Impl[A] @unchecked =>
// append valid things in the block to acc
for (i <- impl.offset until BlockSize) {
builder += impl.block(i)
}
loop(impl.tailBList)
}
loop(this)
}

def isEmpty: Boolean = false
def iterator: Iterator[A] = new BListIterator(this)

private[collections] def toStringInBlocks: String = {
val strb = new java.lang.StringBuilder
strb.append("BList(")
@tailrec
def loop(first: Boolean, l: BList[A]): Unit = {
if (!first) strb.append(", "): Unit
l match {
case Empty => strb.append("Empty"): Unit
case impl: Impl[A] @unchecked =>
strb.append("Block(")
strb.append(impl.block(impl.offset).toString)
for (i <- impl.offset + 1 until BlockSize) {
strb.append(", ")
strb.append(impl.block(i).toString)
}
strb.append(")")
loop(false, impl.tailBList)
}
}
loop(true, this)
strb.append(")")
strb.toString
}
final private class BListIterator(var curNode: Impl[A @uncheckedVariance]) extends Iterator[A] {
var curOffset: Int = curNode.offset

def hasNext: Boolean = curOffset < BlockSize
def next(): A =
if (curOffset >= BlockSize) {
throw new NoSuchElementException
} else {
val next_elmt = curNode.block(curOffset)
curOffset += 1
if (curOffset >= BlockSize) {
// try to advance to next block
curNode.tailBList match {
case impl: Impl[A] =>
curOffset = impl.offset
curNode = impl
case Empty => // nothing, keep offset at blocksize
}
}
next_elmt
}
}
}

def fromList[A](l: List[A]): BList[A] =
fromListReverse(l.reverse)

def fromListReverse[A](l: List[A]): BList[A] = {
@tailrec
def go(l: List[A], acc: BList[A]): BList[A] = {
l match {
case Nil => acc
case h :: t => go(t, acc.prepend(h))
}
}
go(l, empty)
}

def empty[A]: BList[A] = Empty

}
Loading
Loading