From 116687a11a49467576406aebde41015abf275940 Mon Sep 17 00:00:00 2001 From: Lola Denney Date: Thu, 28 May 2026 11:34:37 -0400 Subject: [PATCH 01/36] foundations for BList --- .../main/scala/cats/collections/BList.scala | 236 ++++++++++++++++++ .../scala/cats/collections/BListSuite.scala | 1 + 2 files changed, 237 insertions(+) create mode 100644 core/src/main/scala/cats/collections/BList.scala create mode 100644 tests/src/test/scala/cats/collections/BListSuite.scala diff --git a/core/src/main/scala/cats/collections/BList.scala b/core/src/main/scala/cats/collections/BList.scala new file mode 100644 index 00000000..03829e28 --- /dev/null +++ b/core/src/main/scala/cats/collections/BList.scala @@ -0,0 +1,236 @@ +import scala.annotation.tailrec + +sealed abstract class BList[+A] { + def uncons: Option[(A, BList[A])] + def prepend[B >: A](a: B): BList[B] + def headOption: Option[A] + def tailOption: Option[BList[A]] + def get(idx: Long): Option[A] + def getUnsafe(idx: Long): A + def lastOption: Option[A] // a bit more efficient than get(size-1) + 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 strictFoldRight[B](fin: B)(fn: (A, B) => B): B + // def combineK + + override def toString: String = { // go back and optimize with blocks + 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 + } +} + +object BList { + private val BlockSize = 4 // test with different values + case object Empty extends BList[Nothing] { + def uncons = None + def prepend[A](a: A): BList[A] = { // why would we put the element at the end of a block? dont we want to fill from start out. in this case we would want head and tail pointer + val ary = (new Array[Any](BlockSize)).asInstanceOf[Array[A]] + val offset = BlockSize - 1 + ary(offset) = a + Impl(offset, ary, Empty) + } + def headOption: Option[Nothing] = None + def tailOption: Option[Nothing] = None + def get(idx: Long): Option[Nothing] = None + def getUnsafe(idx: Long): Nothing = throw new NoSuchElementException("invalid index") + def lastOption: Option[Nothing] = 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 strictFoldRight[B](acc: B)(fn: (Nothing, B) => B): B = acc + + } + sealed abstract class NonEmpty[+A] extends BList[A] { + // TODO can put methods in here that are only safe for nonempty (ex. head, reduce) + def head: A + // def tail:BList[A] + // def reduce + } + + private object Impl { + def apply[A](offset: Int, block: Array[Any], tail: BList[A]): Impl[A] = + new Impl(offset, block.asInstanceOf[Array[A]], tail) + + } + + // (maybe impl will be covariant or not) + private case class Impl[A](offset: Int, block: Array[A], tail: BList[A]) extends NonEmpty[A] { + def uncons = { + val nextOffset = offset + 1 + val next = if (nextOffset == block.length) tail else Impl(nextOffset, block, tail) + Some((block(offset).asInstanceOf[A], next)) + } + def prepend[B >: A](a: B): BList[B] = { + + if (offset > 0) { + // copy the right side + val ary = block.clone() // replaced with copyof method to prevent having to zero out the memory first + val nextOffset = offset - 1 + ary(nextOffset) = + a.asInstanceOf[A] // dont know if i like as instance of. because a is a B here, but ary:Array[A] + Impl(nextOffset, ary, tail) + } else { + val ary = new Array[Any](BlockSize) + val offset = BlockSize - 1 + ary(offset) = a + Impl(offset, ary, this) + } + } + def head: A = { + block(offset) + } + def headOption: Option[A] = { + Some(block(offset)) + } + def tailOption: Option[BList[A]] = { + if (offset < BlockSize - 1) { + Some(Impl(offset + 1, block, tail)) + } else { + 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(offset, block, tail) => + if (idx < BlockSize - offset) { + Some(block(offset + idx.toInt).asInstanceOf[A]) + } else { + go(idx - (BlockSize - offset), tail) + } + } + } + go(idx, this) + } + } + def getUnsafe(idx: Long): A = { + if (idx < 0) { + throw new IndexOutOfBoundsException + } else { + @tailrec + def go(idx: Long, l: BList[A]): A = { + l match { + case Empty => throw new NoSuchElementException("invalid index") + case Impl(offset, block, tail) => + if (idx < BlockSize - offset) { + block(offset + idx.toInt).asInstanceOf[A] + } else { + go(idx - (BlockSize - offset), tail) + } + } + } + go(idx, this) + } + } + def lastOption: Option[A] = { + @tailrec + def go(l: BList[A]): Option[A] = { + l.asInstanceOf[Impl[A]] match { + case Impl(_, block, tail) => + tail match { + case Empty => Some(block(BlockSize - 1).asInstanceOf[A]) + case Impl(_, _, _) => go(tail) + } + } + } + go(this) + } + def size: Long = { + @tailrec + def loop(l: BList[A], acc: Long): Long = { + l match { + case Empty => acc + case Impl(offset, _, tail) => loop(tail, acc + (BlockSize - offset)) + } + } + loop(this, 0L) + } + def map[B](fn: A => B): BList[B] = { + val ary = new Array[Any](BlockSize) + System.arraycopy(block, offset, ary, offset, BlockSize - offset) + var i = 0 + while (i < ary.length) { + ary(i) = fn(ary(i).asInstanceOf[A]) + i += 1 + } + Impl(offset, ary, tail.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(offset, block, tail) => + var newacc = acc + var i = offset + while (i < block.length) { + newacc = fn(newacc, block(i).asInstanceOf[A]) + i += 1 + } + loop(newacc, tail) + } + loop(acc, this) + } + def drop(n: Long): BList[A] = { + @tailrec + def go(n: Long, l: BList[A]): BList[A] = { + l match { + case Empty => + Empty + case Impl(offset, block, tail) => + if (n >= BlockSize - offset) { + go(n - (BlockSize - offset), tail) + } else { + val m: Int = math.max(n.toInt, 0) // drop < 0 is the same as drop 0 + val ary = (new Array[Any](BlockSize)).asInstanceOf[Array[A]] + System.arraycopy(block, offset + m, ary, offset + m, BlockSize - (offset + m)) + Impl(offset + m, ary, tail) + } + } + } + go(n, this) + } + /* + def strictFoldRight[B](acc: B)(fn: (A, B) => B): B = { + // TODO + val fnblock = (block:Array[Any],offset:Int,acc1:B ) => { + // get only the useful part of the block + val ary : Array[A] = block.drop(offset).map(_.asInstanceOf[A]) + ary.foldRight(acc1)(fn) //fold right on the block to produce new accumulator + } + + def go(l:BList[A],acc: B,fnblock: (Array[Any], Int, B) => B): B = { + l match { + case Empty => acc + case Impl(offset,block, tail)=> fnblock(block,offset, go(tail, acc, fnblock)) + } + } + go(this, acc, fnblock) + } + */ + + } + + def empty[A]: BList[A] = Empty + def unapply[A](l: BList[A]): Option[(A, BList[A])] = l.uncons +} diff --git a/tests/src/test/scala/cats/collections/BListSuite.scala b/tests/src/test/scala/cats/collections/BListSuite.scala new file mode 100644 index 00000000..84eb2a66 --- /dev/null +++ b/tests/src/test/scala/cats/collections/BListSuite.scala @@ -0,0 +1 @@ +package cats.collections From f8b1a1611840054f757d9bf0bb3831d27de1a107 Mon Sep 17 00:00:00 2001 From: L Denney <156242030+loladenney@users.noreply.github.com> Date: Thu, 28 May 2026 13:27:25 -0400 Subject: [PATCH 02/36] Update core/src/main/scala/cats/collections/BList.scala Co-authored-by: Sergey Torgashov --- core/src/main/scala/cats/collections/BList.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/src/main/scala/cats/collections/BList.scala b/core/src/main/scala/cats/collections/BList.scala index 03829e28..daae4424 100644 --- a/core/src/main/scala/cats/collections/BList.scala +++ b/core/src/main/scala/cats/collections/BList.scala @@ -124,11 +124,11 @@ object BList { } } def getUnsafe(idx: Long): A = { - if (idx < 0) { + if (idx < 0) throw new IndexOutOfBoundsException - } else { - @tailrec - def go(idx: Long, l: BList[A]): A = { + + @tailrec + def go(idx: Long, l: BList[A]): A = { l match { case Empty => throw new NoSuchElementException("invalid index") case Impl(offset, block, tail) => From 877d3d62d0a89dc8042fd09cc1e594ce983a5ac5 Mon Sep 17 00:00:00 2001 From: Lola Denney Date: Fri, 29 May 2026 11:56:02 -0400 Subject: [PATCH 03/36] some more methods --- .../main/scala/cats/collections/BList.scala | 108 ++++++++++++++---- .../scala/cats/collections/BListSuite.scala | 28 +++++ 2 files changed, 113 insertions(+), 23 deletions(-) diff --git a/core/src/main/scala/cats/collections/BList.scala b/core/src/main/scala/cats/collections/BList.scala index daae4424..cd9f028e 100644 --- a/core/src/main/scala/cats/collections/BList.scala +++ b/core/src/main/scala/cats/collections/BList.scala @@ -1,3 +1,24 @@ +/* + * 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. + */ + import scala.annotation.tailrec sealed abstract class BList[+A] { @@ -12,8 +33,15 @@ sealed abstract class BList[+A] { 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 strictFoldRight[B](fin: B)(fn: (A, B) => B): B - // def combineK + def combineK [B >: A](l2: BList[B]): BList[B] + def toList: List[A] + //def strictFoldRight[B](fin: B)(fn: (A, B) => B): B + //final def take(n: Long): TreeList[A] = + + final def ::[B >: A](a: B): BList[B] = prepend(a) + + final def ++[B >: A](l2: BList[B]): BList[B] = combineK(l2) + override def toString: String = { // go back and optimize with blocks val strb = new java.lang.StringBuilder @@ -39,7 +67,7 @@ object BList { case object Empty extends BList[Nothing] { def uncons = None def prepend[A](a: A): BList[A] = { // why would we put the element at the end of a block? dont we want to fill from start out. in this case we would want head and tail pointer - val ary = (new Array[Any](BlockSize)).asInstanceOf[Array[A]] + val ary = new Array[Any](BlockSize) val offset = BlockSize - 1 ary(offset) = a Impl(offset, ary, Empty) @@ -53,6 +81,8 @@ object BList { 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 combineK[B](l2: BList[B]): BList[B] = l2 + def toList: List[Nothing] = Nil // def strictFoldRight[B](acc: B)(fn: (Nothing, B) => B): B = acc } @@ -64,6 +94,8 @@ object BList { } private object Impl { + def apply[A](offset: Int, block: Array[A], tail: BList[A]): Impl[A] = + new Impl(offset, block, tail) def apply[A](offset: Int, block: Array[Any], tail: BList[A]): Impl[A] = new Impl(offset, block.asInstanceOf[Array[A]], tail) @@ -80,13 +112,12 @@ object BList { if (offset > 0) { // copy the right side - val ary = block.clone() // replaced with copyof method to prevent having to zero out the memory first + val ary = block.clone().asInstanceOf[Array[B]] // replaced with copyof method to prevent having to zero out the memory first val nextOffset = offset - 1 - ary(nextOffset) = - a.asInstanceOf[A] // dont know if i like as instance of. because a is a B here, but ary:Array[A] + ary(nextOffset) = a Impl(nextOffset, ary, tail) } else { - val ary = new Array[Any](BlockSize) + val ary = block.clone().asInstanceOf[Array[B]] val offset = BlockSize - 1 ary(offset) = a Impl(offset, ary, this) @@ -129,18 +160,17 @@ object BList { @tailrec def go(idx: Long, l: BList[A]): A = { - l match { - case Empty => throw new NoSuchElementException("invalid index") - case Impl(offset, block, tail) => - if (idx < BlockSize - offset) { - block(offset + idx.toInt).asInstanceOf[A] - } else { - go(idx - (BlockSize - offset), tail) - } - } + l match { + case Empty => throw new NoSuchElementException("invalid index") + case Impl(offset, block, tail) => + if (idx < BlockSize - offset) { + block(offset + idx.toInt).asInstanceOf[A] + } else { + go(idx - (BlockSize - offset), tail) + } } - go(idx, this) } + go(idx, this) } def lastOption: Option[A] = { @tailrec @@ -165,12 +195,11 @@ object BList { } loop(this, 0L) } - def map[B](fn: A => B): BList[B] = { - val ary = new Array[Any](BlockSize) - System.arraycopy(block, offset, ary, offset, BlockSize - offset) + def map[B](fn: A => B): BList[B] = { + val ary = block.clone().asInstanceOf[Array[B]] var i = 0 while (i < ary.length) { - ary(i) = fn(ary(i).asInstanceOf[A]) + ary(i) = fn(block(i)) i += 1 } Impl(offset, ary, tail.map(fn)) @@ -202,14 +231,47 @@ object BList { go(n - (BlockSize - offset), tail) } else { val m: Int = math.max(n.toInt, 0) // drop < 0 is the same as drop 0 - val ary = (new Array[Any](BlockSize)).asInstanceOf[Array[A]] - System.arraycopy(block, offset + m, ary, offset + m, BlockSize - (offset + m)) + val ary = block + .clone() // clone and new offset should do the trick because api doesnt expose the arrays so you can only access after offset Impl(offset + m, ary, tail) } } } go(n, this) } + def combineK[B >: A](l2: BList[B]): BList[B] = + { + // TODO add special cases for if the block is a certain amount empty to shuffle things over + // maybe use benchmarking to find out optimal number here?? + + // copy l1/this and have it's tail be replaced with l2 + //@tailrec !!! not tail rec rn !!! + def go(l: BList[A]): BList[B] = { + l.asInstanceOf[Impl[A]] match { + case Impl(offset, block, tail) => + tail match { + case Empty => l2 + case Impl(_, _, _) => + val ary = block.clone().asInstanceOf[Array[B]] + Impl(offset, ary, go(tail)) + } + } + } + + // for now only check if both are empty + (this, l2) match { + case (_, Empty) => this + case (_,_) => go(this) + + } + } + + def toList: List[A] = { + // TODO OBVIOUSLY NOT NIL + Nil + + } + /* def strictFoldRight[B](acc: B)(fn: (A, B) => B): B = { // TODO diff --git a/tests/src/test/scala/cats/collections/BListSuite.scala b/tests/src/test/scala/cats/collections/BListSuite.scala index 84eb2a66..5a12904f 100644 --- a/tests/src/test/scala/cats/collections/BListSuite.scala +++ b/tests/src/test/scala/cats/collections/BListSuite.scala @@ -1 +1,29 @@ +/* + * 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 munit.DisciplineSuite + +class BListSuite extends DiscipineSuite{ + DefaultScalaCheckPropertyCheckConfig.default + +} \ No newline at end of file From 9172ebe1860e6fb0ffe7dda364caf6ae3d06070f Mon Sep 17 00:00:00 2001 From: L Denney <156242030+loladenney@users.noreply.github.com> Date: Tue, 2 Jun 2026 10:13:22 -0400 Subject: [PATCH 04/36] Update core/src/main/scala/cats/collections/BList.scala Co-authored-by: Sergey Torgashov --- core/src/main/scala/cats/collections/BList.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/scala/cats/collections/BList.scala b/core/src/main/scala/cats/collections/BList.scala index cd9f028e..5202df9e 100644 --- a/core/src/main/scala/cats/collections/BList.scala +++ b/core/src/main/scala/cats/collections/BList.scala @@ -23,7 +23,7 @@ import scala.annotation.tailrec sealed abstract class BList[+A] { def uncons: Option[(A, BList[A])] - def prepend[B >: A](a: B): BList[B] + def prepend[B >: A](a: B): BList.NonEmpty[B] def headOption: Option[A] def tailOption: Option[BList[A]] def get(idx: Long): Option[A] From 7224246374421b1d6eef4cdc57029c4618bde652 Mon Sep 17 00:00:00 2001 From: L Denney <156242030+loladenney@users.noreply.github.com> Date: Tue, 2 Jun 2026 10:13:34 -0400 Subject: [PATCH 05/36] Update core/src/main/scala/cats/collections/BList.scala Co-authored-by: Sergey Torgashov --- core/src/main/scala/cats/collections/BList.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/scala/cats/collections/BList.scala b/core/src/main/scala/cats/collections/BList.scala index 5202df9e..707d7449 100644 --- a/core/src/main/scala/cats/collections/BList.scala +++ b/core/src/main/scala/cats/collections/BList.scala @@ -72,8 +72,8 @@ object BList { ary(offset) = a Impl(offset, ary, Empty) } - def headOption: Option[Nothing] = None - def tailOption: Option[Nothing] = None + def headOption: None.type = None + def tailOption: None.type = None def get(idx: Long): Option[Nothing] = None def getUnsafe(idx: Long): Nothing = throw new NoSuchElementException("invalid index") def lastOption: Option[Nothing] = None From 971943bbdd1382c99b318d56a068d57c59ded0d2 Mon Sep 17 00:00:00 2001 From: Lola Denney Date: Tue, 2 Jun 2026 12:50:44 -0400 Subject: [PATCH 06/36] fixes to drop and toList --- .../main/scala/cats/collections/BList.scala | 97 +++++++++++-------- .../scala/cats/collections/BListSuite.scala | 6 +- 2 files changed, 59 insertions(+), 44 deletions(-) diff --git a/core/src/main/scala/cats/collections/BList.scala b/core/src/main/scala/cats/collections/BList.scala index 707d7449..3c1f81c9 100644 --- a/core/src/main/scala/cats/collections/BList.scala +++ b/core/src/main/scala/cats/collections/BList.scala @@ -33,15 +33,14 @@ sealed abstract class BList[+A] { 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] + def combineK[B >: A](l2: BList[B]): BList[B] def toList: List[A] - //def strictFoldRight[B](fin: B)(fn: (A, B) => B): B - //final def take(n: Long): TreeList[A] = + // def strictFoldRight[B](fin: B)(fn: (A, B) => B): B + // final def take(n: Long): TreeList[A] = final def ::[B >: A](a: B): BList[B] = prepend(a) final def ++[B >: A](l2: BList[B]): BList[B] = combineK(l2) - override def toString: String = { // go back and optimize with blocks val strb = new java.lang.StringBuilder @@ -66,7 +65,7 @@ object BList { private val BlockSize = 4 // test with different values case object Empty extends BList[Nothing] { def uncons = None - def prepend[A](a: A): BList[A] = { // why would we put the element at the end of a block? dont we want to fill from start out. in this case we would want head and tail pointer + def prepend[B >: Nothing](a: B): BList.NonEmpty[B] = { // why would we put the element at the end of a block? dont we want to fill from start out. in this case we would want head and tail pointer val ary = new Array[Any](BlockSize) val offset = BlockSize - 1 ary(offset) = a @@ -74,15 +73,15 @@ object BList { } def headOption: None.type = None def tailOption: None.type = None - def get(idx: Long): Option[Nothing] = None + def get(idx: Long): None.type = None def getUnsafe(idx: Long): Nothing = throw new NoSuchElementException("invalid index") - def lastOption: Option[Nothing] = None + 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 combineK[B](l2: BList[B]): BList[B] = l2 - def toList: List[Nothing] = Nil + override def toList: List[Nothing] = Nil // def strictFoldRight[B](acc: B)(fn: (Nothing, B) => B): B = acc } @@ -108,13 +107,15 @@ object BList { val next = if (nextOffset == block.length) tail else Impl(nextOffset, block, tail) Some((block(offset).asInstanceOf[A], next)) } - def prepend[B >: A](a: B): BList[B] = { + def prepend[B >: A](a: B): BList.NonEmpty[B] = { if (offset > 0) { // copy the right side - val ary = block.clone().asInstanceOf[Array[B]] // replaced with copyof method to prevent having to zero out the memory first + val ary = block + .clone() + .asInstanceOf[Array[B]] // replaced with copyof method to prevent having to zero out the memory first val nextOffset = offset - 1 - ary(nextOffset) = a + ary(nextOffset) = a Impl(nextOffset, ary, tail) } else { val ary = block.clone().asInstanceOf[Array[B]] @@ -195,9 +196,9 @@ object BList { } loop(this, 0L) } - def map[B](fn: A => B): BList[B] = { + def map[B](fn: A => B): BList[B] = { val ary = block.clone().asInstanceOf[Array[B]] - var i = 0 + var i = offset while (i < ary.length) { ary(i) = fn(block(i)) i += 1 @@ -231,47 +232,59 @@ object BList { go(n - (BlockSize - offset), tail) } else { val m: Int = math.max(n.toInt, 0) // drop < 0 is the same as drop 0 - val ary = block - .clone() // clone and new offset should do the trick because api doesnt expose the arrays so you can only access after offset - Impl(offset + m, ary, tail) + if (m == 0) { // no copy needs to happen + Impl(offset, block, tail) + } else { + val ary = new Array[Any](BlockSize) + System.arraycopy(block, offset + m, ary, offset + m, BlockSize - (offset + m)) + Impl(offset + m, ary, tail) + } } } } go(n, this) } - def combineK[B >: A](l2: BList[B]): BList[B] = - { - // TODO add special cases for if the block is a certain amount empty to shuffle things over - // maybe use benchmarking to find out optimal number here?? + def combineK[B >: A](l2: BList[B]): BList[B] = { + // TODO add special cases for if the block is a certain amount empty to shuffle things over + // maybe use benchmarking to find out optimal number here?? - // copy l1/this and have it's tail be replaced with l2 - //@tailrec !!! not tail rec rn !!! - def go(l: BList[A]): BList[B] = { - l.asInstanceOf[Impl[A]] match { - case Impl(offset, block, tail) => - tail match { - case Empty => l2 - case Impl(_, _, _) => - val ary = block.clone().asInstanceOf[Array[B]] - Impl(offset, ary, go(tail)) - } + // @tailrec !!! not tail rec rn, could use cps but maybe ask first !!! + def go(l: BList[A]): BList[B] = { + l.asInstanceOf[Impl[A]] match { + case Impl(offset, block, tail) => + tail match { + case Empty => Impl(offset, block.asInstanceOf[Array[B]], l2) // and the current block!!! + case Impl(_, + _, + _ + ) => // maybe i shouldnt copy the block because no changes are occuring. any operation that needs to change blocks will copy, so realistically two lists can share an array if they both never mutate it + Impl(offset, block.asInstanceOf[Array[B]], go(tail)) + } + } } - } - // for now only check if both are empty - (this, l2) match { - case (_, Empty) => this - case (_,_) => go(this) + // for now only optimization is checking if either are empty + (this, l2) match { + case (_, Empty) => this + case (_, _) => go(this) + } } - } - def toList: List[A] = { - // TODO OBVIOUSLY NOT NIL - Nil + 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(offset, block, tail) => + // append valid things in the block to acc + builder ++= block.slice(offset, BlockSize).toList + loop(tail) + } + loop(this) + } - } - /* def strictFoldRight[B](acc: B)(fn: (A, B) => B): B = { // TODO diff --git a/tests/src/test/scala/cats/collections/BListSuite.scala b/tests/src/test/scala/cats/collections/BListSuite.scala index 5a12904f..f5e0f490 100644 --- a/tests/src/test/scala/cats/collections/BListSuite.scala +++ b/tests/src/test/scala/cats/collections/BListSuite.scala @@ -22,8 +22,10 @@ package cats.collections import munit.DisciplineSuite +import org.scalacheck.Test -class BListSuite extends DiscipineSuite{ +class BListSuite extends DisciplineSuite { + override def scalaCheckTestParameters: Test.Parameters = DefaultScalaCheckPropertyCheckConfig.default -} \ No newline at end of file +} From a1a5d9ac3d367012d21dc786b454baa817fe752c Mon Sep 17 00:00:00 2001 From: Lola Denney Date: Thu, 4 Jun 2026 09:53:38 -0400 Subject: [PATCH 07/36] removed explicit typecasts and changed tail to tailBList --- .../main/scala/cats/collections/BList.scala | 80 +++++++++---------- 1 file changed, 38 insertions(+), 42 deletions(-) diff --git a/core/src/main/scala/cats/collections/BList.scala b/core/src/main/scala/cats/collections/BList.scala index 3c1f81c9..5d9470e7 100644 --- a/core/src/main/scala/cats/collections/BList.scala +++ b/core/src/main/scala/cats/collections/BList.scala @@ -93,32 +93,28 @@ object BList { } private object Impl { - def apply[A](offset: Int, block: Array[A], tail: BList[A]): Impl[A] = - new Impl(offset, block, tail) - def apply[A](offset: Int, block: Array[Any], tail: BList[A]): Impl[A] = - new Impl(offset, block.asInstanceOf[Array[A]], tail) + 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) } // (maybe impl will be covariant or not) - private case class Impl[A](offset: Int, block: Array[A], tail: BList[A]) extends NonEmpty[A] { + private case class Impl[A](offset: Int, block: Array[A], tailBList: BList[A]) extends NonEmpty[A] { def uncons = { val nextOffset = offset + 1 - val next = if (nextOffset == block.length) tail else Impl(nextOffset, block, tail) - Some((block(offset).asInstanceOf[A], next)) + val next = if (nextOffset == block.length) tailBList else Impl(nextOffset, block, tailBList) + Some((block(offset), next)) } def prepend[B >: A](a: B): BList.NonEmpty[B] = { - if (offset > 0) { - // copy the right side - val ary = block - .clone() - .asInstanceOf[Array[B]] // replaced with copyof method to prevent having to zero out the memory first + val ary = block.clone().asInstanceOf[Array[B]] val nextOffset = offset - 1 ary(nextOffset) = a - Impl(nextOffset, ary, tail) + Impl(nextOffset, ary, tailBList) } else { - val ary = block.clone().asInstanceOf[Array[B]] + val ary = new Array[Any](BlockSize) // need a blank one for a new block val offset = BlockSize - 1 ary(offset) = a Impl(offset, ary, this) @@ -132,9 +128,9 @@ object BList { } def tailOption: Option[BList[A]] = { if (offset < BlockSize - 1) { - Some(Impl(offset + 1, block, tail)) + Some(Impl(offset + 1, block, tailBList)) } else { - Some(tail) + Some(tailBList) } } def get(idx: Long): Option[A] = { @@ -144,11 +140,11 @@ object BList { def go(idx: Long, l: BList[A]): Option[A] = { l match { case Empty => None - case Impl(offset, block, tail) => + case Impl(offset, block, tailBList) => if (idx < BlockSize - offset) { - Some(block(offset + idx.toInt).asInstanceOf[A]) + Some(block(offset + idx.toInt)) } else { - go(idx - (BlockSize - offset), tail) + go(idx - (BlockSize - offset), tailBList) } } } @@ -163,11 +159,11 @@ object BList { def go(idx: Long, l: BList[A]): A = { l match { case Empty => throw new NoSuchElementException("invalid index") - case Impl(offset, block, tail) => + case Impl(offset, block, tailBList) => if (idx < BlockSize - offset) { - block(offset + idx.toInt).asInstanceOf[A] + block(offset + idx.toInt) } else { - go(idx - (BlockSize - offset), tail) + go(idx - (BlockSize - offset), tailBList) } } } @@ -177,10 +173,10 @@ object BList { @tailrec def go(l: BList[A]): Option[A] = { l.asInstanceOf[Impl[A]] match { - case Impl(_, block, tail) => - tail match { - case Empty => Some(block(BlockSize - 1).asInstanceOf[A]) - case Impl(_, _, _) => go(tail) + case Impl(_, block, tailBList) => + tailBList match { + case Empty => Some(block(BlockSize - 1)) + case Impl(_, _, _) => go(tailBList) } } } @@ -191,7 +187,7 @@ object BList { def loop(l: BList[A], acc: Long): Long = { l match { case Empty => acc - case Impl(offset, _, tail) => loop(tail, acc + (BlockSize - offset)) + case Impl(offset, _, tailBList) => loop(tailBList, acc + (BlockSize - offset)) } } loop(this, 0L) @@ -203,21 +199,21 @@ object BList { ary(i) = fn(block(i)) i += 1 } - Impl(offset, ary, tail.map(fn)) + 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(offset, block, tail) => + case Impl(offset, block, tailBList) => var newacc = acc var i = offset while (i < block.length) { - newacc = fn(newacc, block(i).asInstanceOf[A]) + newacc = fn(newacc, block(i)) i += 1 } - loop(newacc, tail) + loop(newacc, tailBList) } loop(acc, this) } @@ -227,17 +223,17 @@ object BList { l match { case Empty => Empty - case Impl(offset, block, tail) => + case Impl(offset, block, tailBList) => if (n >= BlockSize - offset) { - go(n - (BlockSize - offset), tail) + go(n - (BlockSize - offset), tailBList) } else { val m: Int = math.max(n.toInt, 0) // drop < 0 is the same as drop 0 if (m == 0) { // no copy needs to happen - Impl(offset, block, tail) + l } else { val ary = new Array[Any](BlockSize) System.arraycopy(block, offset + m, ary, offset + m, BlockSize - (offset + m)) - Impl(offset + m, ary, tail) + Impl(offset + m, ary, tailBList) } } } @@ -248,17 +244,17 @@ object BList { // TODO add special cases for if the block is a certain amount empty to shuffle things over // maybe use benchmarking to find out optimal number here?? - // @tailrec !!! not tail rec rn, could use cps but maybe ask first !!! + // @tailrec !!! not tailBListrec rn, could use cps but maybe ask first !!! def go(l: BList[A]): BList[B] = { l.asInstanceOf[Impl[A]] match { - case Impl(offset, block, tail) => - tail match { + case Impl(offset, block, tailBList) => + tailBList match { case Empty => Impl(offset, block.asInstanceOf[Array[B]], l2) // and the current block!!! case Impl(_, _, _ ) => // maybe i shouldnt copy the block because no changes are occuring. any operation that needs to change blocks will copy, so realistically two lists can share an array if they both never mutate it - Impl(offset, block.asInstanceOf[Array[B]], go(tail)) + Impl(offset, block.asInstanceOf[Array[B]], go(tailBList)) } } } @@ -277,10 +273,10 @@ object BList { def loop(l: BList[A]): List[A] = l match { case Empty => builder.result() - case Impl(offset, block, tail) => + case Impl(offset, block, tailBList) => // append valid things in the block to acc builder ++= block.slice(offset, BlockSize).toList - loop(tail) + loop(tailBList) } loop(this) } @@ -297,7 +293,7 @@ object BList { def go(l:BList[A],acc: B,fnblock: (Array[Any], Int, B) => B): B = { l match { case Empty => acc - case Impl(offset,block, tail)=> fnblock(block,offset, go(tail, acc, fnblock)) + case Impl(offset,block, tailBList)=> fnblock(block,offset, go(tailBList, acc, fnblock)) } } go(this, acc, fnblock) From f9e6c8bc3a91d6a6e74343e5382a6edd822dea3e Mon Sep 17 00:00:00 2001 From: Lola Denney Date: Thu, 4 Jun 2026 10:53:16 -0400 Subject: [PATCH 08/36] made nonempty object, and other changes addressing comments --- .../main/scala/cats/collections/BList.scala | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/core/src/main/scala/cats/collections/BList.scala b/core/src/main/scala/cats/collections/BList.scala index 5d9470e7..fc2b4af4 100644 --- a/core/src/main/scala/cats/collections/BList.scala +++ b/core/src/main/scala/cats/collections/BList.scala @@ -88,16 +88,20 @@ object BList { sealed abstract class NonEmpty[+A] extends BList[A] { // TODO can put methods in here that are only safe for nonempty (ex. head, reduce) def head: A - // def tail:BList[A] + def tail: BList[A] // def reduce } + object NonEmpty { + def apply[A](h: A, t: BList[A]): NonEmpty[A] = t.prepend(h) + def unapply[A](l: BList[A]): Option[(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) - } // (maybe impl will be covariant or not) @@ -123,15 +127,18 @@ object BList { def head: A = { block(offset) } + def tail: BList[A] = { + if (offset < BlockSize - 1) { + Impl(offset + 1, block, tailBList) + } else { + tailBList + } + } def headOption: Option[A] = { Some(block(offset)) } def tailOption: Option[BList[A]] = { - if (offset < BlockSize - 1) { - Some(Impl(offset + 1, block, tailBList)) - } else { - Some(tailBList) - } + Some(tail) } def get(idx: Long): Option[A] = { if (idx < 0) { None } @@ -139,7 +146,7 @@ object BList { @tailrec def go(idx: Long, l: BList[A]): Option[A] = { l match { - case Empty => None + case Empty => None case Impl(offset, block, tailBList) => if (idx < BlockSize - offset) { Some(block(offset + idx.toInt)) @@ -158,7 +165,7 @@ object BList { @tailrec def go(idx: Long, l: BList[A]): A = { l match { - case Empty => throw new NoSuchElementException("invalid index") + case Empty => throw new NoSuchElementException("invalid index") case Impl(offset, block, tailBList) => if (idx < BlockSize - offset) { block(offset + idx.toInt) @@ -186,7 +193,7 @@ object BList { @tailrec def loop(l: BList[A], acc: Long): Long = { l match { - case Empty => acc + case Empty => acc case Impl(offset, _, tailBList) => loop(tailBList, acc + (BlockSize - offset)) } } @@ -205,7 +212,7 @@ object BList { @tailrec def loop(acc: B, l: BList[A]): B = l match { - case Empty => acc + case Empty => acc case Impl(offset, block, tailBList) => var newacc = acc var i = offset @@ -272,10 +279,12 @@ object BList { @tailrec def loop(l: BList[A]): List[A] = l match { - case Empty => builder.result() + case Empty => builder.result() case Impl(offset, block, tailBList) => // append valid things in the block to acc - builder ++= block.slice(offset, BlockSize).toList + for (i <- offset until BlockSize) { + builder += block(i) + } loop(tailBList) } loop(this) @@ -303,5 +312,4 @@ object BList { } def empty[A]: BList[A] = Empty - def unapply[A](l: BList[A]): Option[(A, BList[A])] = l.uncons } From 089d8545e2f6a928029c9ab6388630de1929f735 Mon Sep 17 00:00:00 2001 From: Lola Denney Date: Fri, 5 Jun 2026 11:59:09 -0400 Subject: [PATCH 09/36] starting on test suite with individual tests --- .../main/scala/cats/collections/BList.scala | 70 ++++++--- .../scala/cats/collections/BListSuite.scala | 134 ++++++++++++++++++ 2 files changed, 186 insertions(+), 18 deletions(-) diff --git a/core/src/main/scala/cats/collections/BList.scala b/core/src/main/scala/cats/collections/BList.scala index fc2b4af4..181a647d 100644 --- a/core/src/main/scala/cats/collections/BList.scala +++ b/core/src/main/scala/cats/collections/BList.scala @@ -19,6 +19,8 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +package cats.collections + import scala.annotation.tailrec sealed abstract class BList[+A] { @@ -36,7 +38,9 @@ sealed abstract class BList[+A] { def combineK[B >: A](l2: BList[B]): BList[B] def toList: List[A] // def strictFoldRight[B](fin: B)(fn: (A, B) => B): B - // final def take(n: Long): TreeList[A] = + + // for development and testing + def toStringInBlocks: String final def ::[B >: A](a: B): BList[B] = prepend(a) @@ -59,10 +63,12 @@ sealed abstract class BList[+A] { strb.append(")") strb.toString } + } object BList { private 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] = { // why would we put the element at the end of a block? dont we want to fill from start out. in this case we would want head and tail pointer @@ -74,7 +80,7 @@ object BList { def headOption: None.type = None def tailOption: None.type = None def get(idx: Long): None.type = None - def getUnsafe(idx: Long): Nothing = throw new NoSuchElementException("invalid index") + 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 @@ -82,6 +88,7 @@ object BList { def drop(n: Long): BList[Nothing] = Empty def combineK[B](l2: BList[B]): BList[B] = l2 override def toList: List[Nothing] = Nil + def toStringInBlocks: String = "Empty" // def strictFoldRight[B](acc: B)(fn: (Nothing, B) => B): B = acc } @@ -93,7 +100,8 @@ object BList { } object NonEmpty { - def apply[A](h: A, t: BList[A]): NonEmpty[A] = t.prepend(h) + def apply[A](h: A, t: BList[A]): NonEmpty[A] = + t.prepend(h) // maybe make this take arbitary number of args of same type and make a list out of them? def unapply[A](l: BList[A]): Option[(A, BList[A])] = l.uncons } @@ -165,7 +173,7 @@ object BList { @tailrec def go(idx: Long, l: BList[A]): A = { l match { - case Empty => throw new NoSuchElementException("invalid index") + case Empty => throw new IndexOutOfBoundsException case Impl(offset, block, tailBList) => if (idx < BlockSize - offset) { block(offset + idx.toInt) @@ -200,7 +208,7 @@ object BList { loop(this, 0L) } def map[B](fn: A => B): BList[B] = { - val ary = block.clone().asInstanceOf[Array[B]] + val ary = new Array[Any](BlockSize) var i = offset while (i < ary.length) { ary(i) = fn(block(i)) @@ -227,22 +235,23 @@ object BList { def drop(n: Long): BList[A] = { @tailrec def go(n: Long, l: BList[A]): BList[A] = { - l match { - case Empty => - Empty - case Impl(offset, block, tailBList) => - if (n >= BlockSize - offset) { - go(n - (BlockSize - offset), tailBList) - } else { - val m: Int = math.max(n.toInt, 0) // drop < 0 is the same as drop 0 - if (m == 0) { // no copy needs to happen - l + if (n <= 0) { + l + } else { + l match { + case Empty => + Empty + case Impl(offset, block, tailBList) => + if (n >= BlockSize - offset) { + go(n - (BlockSize - offset), tailBList) } else { val ary = new Array[Any](BlockSize) - System.arraycopy(block, offset + m, ary, offset + m, BlockSize - (offset + m)) - Impl(offset + m, ary, tailBList) + val newOffset: Int = offset + n.asInstanceOf[Int] // type conversion safe because 0 strb.append("Empty"): Unit + case Impl(offset, block, tailBList) => + strb.append("Block(") + strb.append(block(offset).toString) + for (i <- offset + 1 until BlockSize) { + strb.append(", ") + strb.append(block(i).toString) + } + strb.append(")") + loop(false, tailBList) + } + } + + loop(true, this) + strb.append(")") + strb.toString + } + /* def strictFoldRight[B](acc: B)(fn: (A, B) => B): B = { // TODO diff --git a/tests/src/test/scala/cats/collections/BListSuite.scala b/tests/src/test/scala/cats/collections/BListSuite.scala index f5e0f490..1be1bf5b 100644 --- a/tests/src/test/scala/cats/collections/BListSuite.scala +++ b/tests/src/test/scala/cats/collections/BListSuite.scala @@ -23,9 +23,143 @@ package cats.collections import munit.DisciplineSuite import org.scalacheck.Test +import scala.math.pow + +object global { + val BlockSize: Int = 4 +} class BListSuite extends DisciplineSuite { override def scalaCheckTestParameters: Test.Parameters = DefaultScalaCheckPropertyCheckConfig.default + test("concatenating to form same list") { + val l1 = BList.empty[Int].prepend(5).prepend(4).prepend(3).prepend(2).prepend(1) + val m1 = BList.empty[Int].prepend(0).prepend(-1).prepend(-2) + + val l2 = BList.empty[Int].prepend(5).prepend(4).prepend(3).prepend(2) + val m2 = BList.empty[Int].prepend(1).prepend(0).prepend(-1).prepend(-2) + + val n1 = m1.combineK(l1) + val n2 = m2.combineK(l2) + + assertEquals(n1.toString, n2.toString) + } + + test("uncons when tail is a new node") { + val l = BList.empty[Int].prepend(4).prepend(3).prepend(2).prepend(1) + val m = BList.empty[Int].prepend(0) + + val n = m.combineK(l) + + n.uncons match { + case None => fail("should not be empty") + case Some((h, t)) => + assertEquals(h, m.head) + assertEquals(t.toString, l.toString) + } + } + + test("prepend when block is full") { + var l = BList.empty[Int] + for (i <- (0 until global.BlockSize).reverse) { + l = l.prepend(i) + } + val m = l.prepend(-1) + assertEquals(m.toList, (-1 until global.BlockSize).toList) + } + + test("headOption on empty list") { + val l = BList.empty[Int] + assertEquals(l.headOption, None) + } + + test("headOption on single element") { + val l = BList.empty[Int].prepend(1) + assertEquals(l.headOption, Some(1)) + } + + test("tailOption on single element") { + val l = BList.empty[Int].prepend(1) + assertEquals(l.tailOption, Some(BList.empty[Int])) + } + + test("tail on single element") { + val l = BList.NonEmpty[Int](1, BList.empty[Int]) + assertEquals(l.tail, BList.empty[Int]) + } + + test("get/getUnsafe on a Blist with incomplete blocks") { + var l = BList.empty[Int] + var m = BList.empty[Int] + for (i <- (0 until global.BlockSize).reverse) { + l = l.prepend(i) + m = l.combineK(m) + } + assertEquals(m.getUnsafe((global.BlockSize + (global.BlockSize - 1)).toLong), 2) + assertEquals(m.get((global.BlockSize + (global.BlockSize - 1)).toLong), Some(2)) + } + test("get/getUnsafe index too small") { + val l = BList.empty[Int].prepend(5).prepend(4).prepend(3).prepend(2).prepend(1) + assertEquals(l.get(-1), None) + intercept[IndexOutOfBoundsException](l.getUnsafe(-1)) + } + test("get/getUnsafe index 0 on empty list") { + val l = BList.empty[Int] + assertEquals(l.get(0), None) + intercept[IndexOutOfBoundsException](l.getUnsafe(0)) + } + test("get/getUnsafe index too big") { + var l = BList.empty[Int] + var m = BList.empty[Int] + for (i <- (0 until global.BlockSize).reverse) { + l = l.prepend(i) + m = l.combineK(m) + } + // size is triangle numbers in blocksize. size should be n(n+1)/2 where n is blocksize + // so blocksize ** 2 + 1 will always be out of bounds + val idx: Long = pow(global.BlockSize.toDouble, 2.0).toLong + 1L + assertEquals(m.get(idx), None) + intercept[IndexOutOfBoundsException](m.getUnsafe(idx)) + } + test("lastOption with incomplete blocks") { + var l = BList.empty[Int] + var m = BList.empty[Int] + for (i <- (0 until global.BlockSize).reverse) { + l = l.prepend(i) + m = l.combineK(m) + } + + // last should be blocksize-1 + assertEquals(m.lastOption, Some(global.BlockSize - 1)) + } + test("lastOption on empty list") { + val l = BList.empty[Int] + assertEquals(l.lastOption, None) + } + test("size on a triangle number construction") { + var l = BList.empty[Int] + var m = BList.empty[Int] + for (i <- (0 until global.BlockSize).reverse) { + l = l.prepend(i) + m = l.combineK(m) + } + // size is triangle numbers in blocksize. formula: n(n+1)/2 + val expected: Long = (global.BlockSize * (global.BlockSize + 1)).toLong / 2L + assertEquals(m.size, expected) + } + test("size on empty") { + val l = BList.empty[Int] + assertEquals(l.size, 0L) + } + + // list of all the specific things i want to test + /* + prepend/uncons are inverses ? + map - ?? + foldleft-?? + drop - + toList - Blist with many blocks + */ + } From 7888e923d3f3a87fa38c84257180489bc4f3641c Mon Sep 17 00:00:00 2001 From: Lola Denney Date: Fri, 5 Jun 2026 13:12:04 -0400 Subject: [PATCH 10/36] addressing Sergey's commments from june 4th --- .../main/scala/cats/collections/BList.scala | 32 +++++++++++-------- .../scala/cats/collections/BListSuite.scala | 14 ++++---- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/core/src/main/scala/cats/collections/BList.scala b/core/src/main/scala/cats/collections/BList.scala index 181a647d..8ff22a4e 100644 --- a/core/src/main/scala/cats/collections/BList.scala +++ b/core/src/main/scala/cats/collections/BList.scala @@ -35,7 +35,7 @@ sealed abstract class BList[+A] { 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] + def concat[B >: A](l2: BList[B]): BList[B] def toList: List[A] // def strictFoldRight[B](fin: B)(fn: (A, B) => B): B @@ -44,7 +44,7 @@ sealed abstract class BList[+A] { final def ::[B >: A](a: B): BList[B] = prepend(a) - final def ++[B >: A](l2: BList[B]): BList[B] = combineK(l2) + final def ++[B >: A](l2: BList[B]): BList[B] = concat(l2) override def toString: String = { // go back and optimize with blocks val strb = new java.lang.StringBuilder @@ -86,7 +86,7 @@ object BList { 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 combineK[B](l2: BList[B]): BList[B] = l2 + def concat[B](l2: BList[B]): BList[B] = l2 override def toList: List[Nothing] = Nil def toStringInBlocks: String = "Empty" // def strictFoldRight[B](acc: B)(fn: (Nothing, B) => B): B = acc @@ -94,9 +94,15 @@ object BList { } sealed abstract class NonEmpty[+A] extends BList[A] { // TODO can put methods in here that are only safe for nonempty (ex. head, reduce) + def uncons: Some[(A, BList[A])] def head: A def tail: BList[A] - // def reduce + def headOption: Some[A] + def tailOption: Some[BList[A]] + def lastOption: Some[A] + def map[B](fn: A => B): BList.NonEmpty[B] + def concat[B >: A](l2: BList[B]): BList.NonEmpty[B] + } object NonEmpty { @@ -114,7 +120,7 @@ object BList { // (maybe impl will be covariant or not) private case class Impl[A](offset: Int, block: Array[A], tailBList: BList[A]) extends NonEmpty[A] { - def uncons = { + def uncons: Some[(A, BList[A])] = { val nextOffset = offset + 1 val next = if (nextOffset == block.length) tailBList else Impl(nextOffset, block, tailBList) Some((block(offset), next)) @@ -142,10 +148,10 @@ object BList { tailBList } } - def headOption: Option[A] = { + def headOption: Some[A] = { Some(block(offset)) } - def tailOption: Option[BList[A]] = { + def tailOption: Some[BList[A]] = { Some(tail) } def get(idx: Long): Option[A] = { @@ -184,9 +190,9 @@ object BList { } go(idx, this) } - def lastOption: Option[A] = { + def lastOption: Some[A] = { @tailrec - def go(l: BList[A]): Option[A] = { + def go(l: BList[A]): Some[A] = { l.asInstanceOf[Impl[A]] match { case Impl(_, block, tailBList) => tailBList match { @@ -207,7 +213,7 @@ object BList { } loop(this, 0L) } - def map[B](fn: A => B): BList[B] = { + def map[B](fn: A => B): BList.NonEmpty[B] = { val ary = new Array[Any](BlockSize) var i = offset while (i < ary.length) { @@ -256,12 +262,12 @@ object BList { } go(n, this) } - def combineK[B >: A](l2: BList[B]): BList[B] = { + def concat[B >: A](l2: BList[B]): BList.NonEmpty[B] = { // TODO add special cases for if the block is a certain amount empty to shuffle things over // maybe use benchmarking to find out optimal number here?? // @tailrec !!! not tailBListrec rn, could use cps but maybe ask first !!! - def go(l: BList[A]): BList[B] = { + def go(l: BList[A]): BList.NonEmpty[B] = { l.asInstanceOf[Impl[A]] match { case Impl(offset, block, tailBList) => tailBList match { @@ -304,7 +310,7 @@ object BList { strb.append("BList(") @tailrec def loop(first: Boolean, l: BList[A]): Unit = { - if (!first) strb.append(", ") + if (!first) strb.append(", "): Unit l match { case Empty => strb.append("Empty"): Unit case Impl(offset, block, tailBList) => diff --git a/tests/src/test/scala/cats/collections/BListSuite.scala b/tests/src/test/scala/cats/collections/BListSuite.scala index 1be1bf5b..622a8dc4 100644 --- a/tests/src/test/scala/cats/collections/BListSuite.scala +++ b/tests/src/test/scala/cats/collections/BListSuite.scala @@ -40,8 +40,8 @@ class BListSuite extends DisciplineSuite { val l2 = BList.empty[Int].prepend(5).prepend(4).prepend(3).prepend(2) val m2 = BList.empty[Int].prepend(1).prepend(0).prepend(-1).prepend(-2) - val n1 = m1.combineK(l1) - val n2 = m2.combineK(l2) + val n1 = m1.concat(l1) + val n2 = m2.concat(l2) assertEquals(n1.toString, n2.toString) } @@ -50,7 +50,7 @@ class BListSuite extends DisciplineSuite { val l = BList.empty[Int].prepend(4).prepend(3).prepend(2).prepend(1) val m = BList.empty[Int].prepend(0) - val n = m.combineK(l) + val n = m.concat(l) n.uncons match { case None => fail("should not be empty") @@ -94,7 +94,7 @@ class BListSuite extends DisciplineSuite { var m = BList.empty[Int] for (i <- (0 until global.BlockSize).reverse) { l = l.prepend(i) - m = l.combineK(m) + m = l.concat(m) } assertEquals(m.getUnsafe((global.BlockSize + (global.BlockSize - 1)).toLong), 2) assertEquals(m.get((global.BlockSize + (global.BlockSize - 1)).toLong), Some(2)) @@ -114,7 +114,7 @@ class BListSuite extends DisciplineSuite { var m = BList.empty[Int] for (i <- (0 until global.BlockSize).reverse) { l = l.prepend(i) - m = l.combineK(m) + m = l.concat(m) } // size is triangle numbers in blocksize. size should be n(n+1)/2 where n is blocksize // so blocksize ** 2 + 1 will always be out of bounds @@ -127,7 +127,7 @@ class BListSuite extends DisciplineSuite { var m = BList.empty[Int] for (i <- (0 until global.BlockSize).reverse) { l = l.prepend(i) - m = l.combineK(m) + m = l.concat(m) } // last should be blocksize-1 @@ -142,7 +142,7 @@ class BListSuite extends DisciplineSuite { var m = BList.empty[Int] for (i <- (0 until global.BlockSize).reverse) { l = l.prepend(i) - m = l.combineK(m) + m = l.concat(m) } // size is triangle numbers in blocksize. formula: n(n+1)/2 val expected: Long = (global.BlockSize * (global.BlockSize + 1)).toLong / 2L From e12726d6c3505e45ef604ae6ac8f598383fa0442 Mon Sep 17 00:00:00 2001 From: Lola Denney Date: Mon, 8 Jun 2026 10:10:14 -0400 Subject: [PATCH 11/36] commit to check in on CI --- core/src/main/scala/cats/collections/BList.scala | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/core/src/main/scala/cats/collections/BList.scala b/core/src/main/scala/cats/collections/BList.scala index 8ff22a4e..a2088be5 100644 --- a/core/src/main/scala/cats/collections/BList.scala +++ b/core/src/main/scala/cats/collections/BList.scala @@ -94,12 +94,8 @@ object BList { } sealed abstract class NonEmpty[+A] extends BList[A] { // TODO can put methods in here that are only safe for nonempty (ex. head, reduce) - def uncons: Some[(A, BList[A])] def head: A def tail: BList[A] - def headOption: Some[A] - def tailOption: Some[BList[A]] - def lastOption: Some[A] def map[B](fn: A => B): BList.NonEmpty[B] def concat[B >: A](l2: BList[B]): BList.NonEmpty[B] @@ -120,7 +116,7 @@ object BList { // (maybe impl will be covariant or not) private case class Impl[A](offset: Int, block: Array[A], tailBList: BList[A]) extends NonEmpty[A] { - def uncons: Some[(A, BList[A])] = { + def uncons: Option[(A, BList[A])] = { val nextOffset = offset + 1 val next = if (nextOffset == block.length) tailBList else Impl(nextOffset, block, tailBList) Some((block(offset), next)) @@ -148,10 +144,10 @@ object BList { tailBList } } - def headOption: Some[A] = { + def headOption: Option[A] = { Some(block(offset)) } - def tailOption: Some[BList[A]] = { + def tailOption: Option[BList[A]] = { Some(tail) } def get(idx: Long): Option[A] = { From 2a7df73fdebd42d50d896319800691654da45224 Mon Sep 17 00:00:00 2001 From: Lola Denney Date: Mon, 8 Jun 2026 12:26:11 -0400 Subject: [PATCH 12/36] more individual tests --- .../scala/cats/collections/BListSuite.scala | 94 +++++++++++++++++-- 1 file changed, 86 insertions(+), 8 deletions(-) diff --git a/tests/src/test/scala/cats/collections/BListSuite.scala b/tests/src/test/scala/cats/collections/BListSuite.scala index 622a8dc4..a9c07c7d 100644 --- a/tests/src/test/scala/cats/collections/BListSuite.scala +++ b/tests/src/test/scala/cats/collections/BListSuite.scala @@ -153,13 +153,91 @@ class BListSuite extends DisciplineSuite { assertEquals(l.size, 0L) } - // list of all the specific things i want to test - /* - prepend/uncons are inverses ? - map - ?? - foldleft-?? - drop - - toList - Blist with many blocks - */ + test("prepend and uncons are inverses") { + val l = BList.empty[Int].prepend(4).prepend(3).prepend(2).prepend(1) + val m = l.prepend(0) + + m.uncons match { + case None => fail("should not be empty") + case Some((h, t)) => + assertEquals(h, 0) + assertEquals(t.toString, l.toString) + } + } + + test("simple map") { + var l = BList.empty[Int] + var m = BList.empty[Int] + for (i <- (0 until 10).reverse) { + l = l.prepend(i) + m = l.concat(m) + } + + val mapped = m.map(_ + 10) + assertEquals(mapped.getUnsafe(20L), m.getUnsafe(20L) + 10) + assertEquals(mapped.getUnsafe(25L), m.getUnsafe(25L) + 10) + } + test("map to a different type") { + var l = BList.empty[Int] + var m = BList.empty[Int] + for (i <- (0 until 10).reverse) { + l = l.prepend(i) + m = l.concat(m) + } + + val mapped = m.map(_ >= 3) + assertEquals(mapped.getUnsafe(20L), m.getUnsafe(20L) >= 3) + assertEquals(mapped.getUnsafe(25L), m.getUnsafe(25L) >= 3) + } + + test("map on empty") { + val l = BList.empty[Int] + assertEquals(l.map(_ + 100), l) + } + + test("foldleft with subtraction") { + var l = BList.empty[Int] + var m = BList.empty[Int] + for (i <- (0 until 10).reverse) { + l = l.prepend(i) + m = l.concat(m) + } + assertEquals(m.foldLeft(m.foldLeft(0)((acc: Int, x: Int) => acc + x))((acc: Int, x: Int) => acc - x), 0) + } + + test("drop on fragmented list") { + var l = BList.empty[Int] + var m = BList.empty[Int] + for (i <- (0 until 10).reverse) { + l = l.prepend(i) + m = l.concat(m) + } + assertEquals(m.drop(10).getUnsafe(15), m.getUnsafe(25)) + assertEquals(m.drop(10).getUnsafe(20), m.getUnsafe(30)) + assertEquals(m.drop(20).getUnsafe(5), m.getUnsafe(25)) + } + test("drop everything") { + var l = BList.empty[Int] + var m = BList.empty[Int] + for (i <- (0 until 4).reverse) { + l = l.prepend(i) + m = l.concat(m) + } + assertEquals(m.drop(10).toString, BList.empty[Int].toString) + assertEquals(m.drop(20).toString, BList.empty[Int].toString) + assertEquals(m.drop(1000).toString, BList.empty[Int].toString) + } + test("drop nothing") { + var l = BList.empty[Int] + var m = BList.empty[Int] + for (i <- (0 until 4).reverse) { + l = l.prepend(i) + m = l.concat(m) + } + + assertEquals(m.drop(0).toString, m.toString) + assertEquals(m.drop(-10).toString, m.toString) + assertEquals(m.drop(-200).toString, m.toString) + } } From 8b02e8a48f6f3d7d932ad3415c7a9ef1551a4ae2 Mon Sep 17 00:00:00 2001 From: Lola Denney Date: Wed, 10 Jun 2026 10:26:04 -0400 Subject: [PATCH 13/36] cleaned up some Impl methods --- .../main/scala/cats/collections/BList.scala | 23 ++++++------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/core/src/main/scala/cats/collections/BList.scala b/core/src/main/scala/cats/collections/BList.scala index a2088be5..12f85053 100644 --- a/core/src/main/scala/cats/collections/BList.scala +++ b/core/src/main/scala/cats/collections/BList.scala @@ -188,13 +188,10 @@ object BList { } def lastOption: Some[A] = { @tailrec - def go(l: BList[A]): Some[A] = { - l.asInstanceOf[Impl[A]] match { - case Impl(_, block, tailBList) => - tailBList match { + def go(self: Impl[A]): Some[A] = { + self.tailBList match { case Empty => Some(block(BlockSize - 1)) - case Impl(_, _, _) => go(tailBList) - } + case next :Impl[A] => go(next) } } go(this) @@ -263,17 +260,11 @@ object BList { // maybe use benchmarking to find out optimal number here?? // @tailrec !!! not tailBListrec rn, could use cps but maybe ask first !!! - def go(l: BList[A]): BList.NonEmpty[B] = { - l.asInstanceOf[Impl[A]] match { - case Impl(offset, block, tailBList) => - tailBList match { + def go(self: Impl[A]): BList.NonEmpty[B] = { + self.tailBList match { case Empty => Impl(offset, block.asInstanceOf[Array[B]], l2) // and the current block!!! - case Impl(_, - _, - _ - ) => // maybe i shouldnt copy the block because no changes are occuring. any operation that needs to change blocks will copy, so realistically two lists can share an array if they both never mutate it - Impl(offset, block.asInstanceOf[Array[B]], go(tailBList)) - } + case next: Impl[A] => // maybe i shouldnt copy the block because no changes are occuring. any operation that needs to change blocks will copy, so realistically two lists can share an array if they both never mutate it + Impl(offset, block.asInstanceOf[Array[B]], go(next)) } } From bdf9d36794b1a5e4d5e00c737d235338e1f4a4c1 Mon Sep 17 00:00:00 2001 From: Lola Denney Date: Wed, 10 Jun 2026 10:56:44 -0400 Subject: [PATCH 14/36] forgot to format on previous commit --- core/src/main/scala/cats/collections/BList.scala | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/src/main/scala/cats/collections/BList.scala b/core/src/main/scala/cats/collections/BList.scala index 12f85053..106416b5 100644 --- a/core/src/main/scala/cats/collections/BList.scala +++ b/core/src/main/scala/cats/collections/BList.scala @@ -190,8 +190,8 @@ object BList { @tailrec def go(self: Impl[A]): Some[A] = { self.tailBList match { - case Empty => Some(block(BlockSize - 1)) - case next :Impl[A] => go(next) + case Empty => Some(block(BlockSize - 1)) + case next: Impl[A] => go(next) } } go(this) @@ -262,9 +262,9 @@ object BList { // @tailrec !!! not tailBListrec rn, could use cps but maybe ask first !!! def go(self: Impl[A]): BList.NonEmpty[B] = { self.tailBList match { - case Empty => Impl(offset, block.asInstanceOf[Array[B]], l2) // and the current block!!! - case next: Impl[A] => // maybe i shouldnt copy the block because no changes are occuring. any operation that needs to change blocks will copy, so realistically two lists can share an array if they both never mutate it - Impl(offset, block.asInstanceOf[Array[B]], go(next)) + case Empty => Impl(offset, block.asInstanceOf[Array[B]], l2) // and the current block!!! + case next: Impl[A] => // maybe i shouldnt copy the block because no changes are occuring. any operation that needs to change blocks will copy, so realistically two lists can share an array if they both never mutate it + Impl(offset, block.asInstanceOf[Array[B]], go(next)) } } From d9a0dee60eb25d31c9adc2528c8a1918d32f7e4b Mon Sep 17 00:00:00 2001 From: Lola Denney Date: Wed, 10 Jun 2026 12:10:29 -0400 Subject: [PATCH 15/36] fixed concat and lastOption --- .../main/scala/cats/collections/BList.scala | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/core/src/main/scala/cats/collections/BList.scala b/core/src/main/scala/cats/collections/BList.scala index 106416b5..85ad9f29 100644 --- a/core/src/main/scala/cats/collections/BList.scala +++ b/core/src/main/scala/cats/collections/BList.scala @@ -98,13 +98,15 @@ object BList { 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])] } object NonEmpty { def apply[A](h: A, t: BList[A]): NonEmpty[A] = t.prepend(h) // maybe make this take arbitary number of args of same type and make a list out of them? - def unapply[A](l: BList[A]): Option[(A, BList[A])] = l.uncons + def unapply[A](l: NonEmpty[A]): Some[(A, BList[A])] = + l.uncons // can i put somethign to garuntee this still be Some } private object Impl { @@ -116,7 +118,7 @@ object BList { // (maybe impl will be covariant or not) private case class Impl[A](offset: Int, block: Array[A], tailBList: BList[A]) extends NonEmpty[A] { - def uncons: Option[(A, BList[A])] = { + def uncons: Some[(A, BList[A])] = { val nextOffset = offset + 1 val next = if (nextOffset == block.length) tailBList else Impl(nextOffset, block, tailBList) Some((block(offset), next)) @@ -190,7 +192,7 @@ object BList { @tailrec def go(self: Impl[A]): Some[A] = { self.tailBList match { - case Empty => Some(block(BlockSize - 1)) + case Empty => Some(self.block(BlockSize - 1)) case next: Impl[A] => go(next) } } @@ -259,12 +261,12 @@ object BList { // TODO add special cases for if the block is a certain amount empty to shuffle things over // maybe use benchmarking to find out optimal number here?? - // @tailrec !!! not tailBListrec rn, could use cps but maybe ask first !!! + // @tailrec !!! not tailBListrec rn, could use cps !!! def go(self: Impl[A]): BList.NonEmpty[B] = { self.tailBList match { - case Empty => Impl(offset, block.asInstanceOf[Array[B]], l2) // and the current block!!! - case next: Impl[A] => // maybe i shouldnt copy the block because no changes are occuring. any operation that needs to change blocks will copy, so realistically two lists can share an array if they both never mutate it - Impl(offset, block.asInstanceOf[Array[B]], go(next)) + case Empty => Impl(self.offset, self.block.asInstanceOf[Array[B]], l2) + case next: Impl[A] => + Impl(self.offset, self.block.asInstanceOf[Array[B]], go(next)) } } @@ -337,6 +339,18 @@ object BList { */ } + 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 } From 69865f63bc55e5819294373a85a0fd62a86e3a35 Mon Sep 17 00:00:00 2001 From: L Denney <156242030+loladenney@users.noreply.github.com> Date: Thu, 11 Jun 2026 09:21:40 -0400 Subject: [PATCH 16/36] Update core/src/main/scala/cats/collections/BList.scala Co-authored-by: Arman Bilge --- core/src/main/scala/cats/collections/BList.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/scala/cats/collections/BList.scala b/core/src/main/scala/cats/collections/BList.scala index 85ad9f29..073d0874 100644 --- a/core/src/main/scala/cats/collections/BList.scala +++ b/core/src/main/scala/cats/collections/BList.scala @@ -67,7 +67,7 @@ sealed abstract class BList[+A] { } object BList { - private val BlockSize = 4 // test with different values + private[collections] final val BlockSize = 4 // test with different values case object Empty extends BList[Nothing] { def uncons = None From 2c30668507181fe21628378afcf54dcdb55166c5 Mon Sep 17 00:00:00 2001 From: Lola Denney Date: Thu, 11 Jun 2026 11:53:11 -0400 Subject: [PATCH 17/36] property tests --- .../main/scala/cats/collections/BList.scala | 53 ++----- .../arbitrary/ArbitraryBList.scala | 61 ++++++++ .../cats/collections/arbitrary/all.scala | 2 + .../cats/collections/arbitrary/package.scala | 2 + .../scala/cats/collections/BListSuite.scala | 132 +++++++++++++++--- 5 files changed, 190 insertions(+), 60 deletions(-) create mode 100644 scalacheck/src/main/scala/cats/collections/arbitrary/ArbitraryBList.scala diff --git a/core/src/main/scala/cats/collections/BList.scala b/core/src/main/scala/cats/collections/BList.scala index 073d0874..f8eaea2a 100644 --- a/core/src/main/scala/cats/collections/BList.scala +++ b/core/src/main/scala/cats/collections/BList.scala @@ -37,10 +37,9 @@ sealed abstract class BList[+A] { def drop(n: Long): BList[A] def concat[B >: A](l2: BList[B]): BList[B] def toList: List[A] - // def strictFoldRight[B](fin: B)(fn: (A, B) => B): B // for development and testing - def toStringInBlocks: String + private[collections] def toStringInBlocks: String final def ::[B >: A](a: B): BList[B] = prepend(a) @@ -67,9 +66,10 @@ sealed abstract class BList[+A] { } object BList { - private[collections] final val BlockSize = 4 // test with different values + final private[collections] val BlockSize = 4 // test with different values case object Empty extends BList[Nothing] { + def unapply[A](l: Empty.type): None.type = None def uncons = None def prepend[B >: Nothing](a: B): BList.NonEmpty[B] = { // why would we put the element at the end of a block? dont we want to fill from start out. in this case we would want head and tail pointer val ary = new Array[Any](BlockSize) @@ -88,8 +88,7 @@ object BList { def drop(n: Long): BList[Nothing] = Empty def concat[B](l2: BList[B]): BList[B] = l2 override def toList: List[Nothing] = Nil - def toStringInBlocks: String = "Empty" - // def strictFoldRight[B](acc: B)(fn: (Nothing, B) => B): B = acc + private[collections] def toStringInBlocks: String = "Empty" } sealed abstract class NonEmpty[+A] extends BList[A] { @@ -104,9 +103,9 @@ object BList { object NonEmpty { def apply[A](h: A, t: BList[A]): NonEmpty[A] = - t.prepend(h) // maybe make this take arbitary number of args of same type and make a list out of them? + t.prepend(h) def unapply[A](l: NonEmpty[A]): Some[(A, BList[A])] = - l.uncons // can i put somethign to garuntee this still be Some + l.uncons } private object Impl { @@ -192,8 +191,8 @@ object BList { @tailrec def go(self: Impl[A]): Some[A] = { self.tailBList match { - case Empty => Some(self.block(BlockSize - 1)) - case next: Impl[A] => go(next) + case Empty => Some(self.block(BlockSize - 1)) + case next: Impl[A] @unchecked => go(next) } } go(this) @@ -258,15 +257,11 @@ object BList { go(n, this) } def concat[B >: A](l2: BList[B]): BList.NonEmpty[B] = { - // TODO add special cases for if the block is a certain amount empty to shuffle things over - // maybe use benchmarking to find out optimal number here?? - - // @tailrec !!! not tailBListrec rn, could use cps !!! + // 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] => - Impl(self.offset, self.block.asInstanceOf[Array[B]], go(next)) + 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)) } } @@ -274,7 +269,6 @@ object BList { (this, l2) match { case (_, Empty) => this case (_, _) => go(this) - } } @@ -294,7 +288,7 @@ object BList { loop(this) } - def toStringInBlocks: String = { + private[collections] def toStringInBlocks: String = { val strb = new java.lang.StringBuilder strb.append("BList(") @tailrec @@ -313,34 +307,15 @@ object BList { loop(false, tailBList) } } - loop(true, this) strb.append(")") strb.toString } - - /* - def strictFoldRight[B](acc: B)(fn: (A, B) => B): B = { - // TODO - val fnblock = (block:Array[Any],offset:Int,acc1:B ) => { - // get only the useful part of the block - val ary : Array[A] = block.drop(offset).map(_.asInstanceOf[A]) - ary.foldRight(acc1)(fn) //fold right on the block to produce new accumulator - } - - def go(l:BList[A],acc: B,fnblock: (Array[Any], Int, B) => B): B = { - l match { - case Empty => acc - case Impl(offset,block, tailBList)=> fnblock(block,offset, go(tailBList, acc, fnblock)) - } - } - go(this, acc, fnblock) - } - */ - } + 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] = { diff --git a/scalacheck/src/main/scala/cats/collections/arbitrary/ArbitraryBList.scala b/scalacheck/src/main/scala/cats/collections/arbitrary/ArbitraryBList.scala new file mode 100644 index 00000000..720e299b --- /dev/null +++ b/scalacheck/src/main/scala/cats/collections/arbitrary/ArbitraryBList.scala @@ -0,0 +1,61 @@ +/* + * 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.arbitrary + +import cats.collections.BList +import org.scalacheck.Arbitrary +import org.scalacheck.Arbitrary.arbitrary +import org.scalacheck.Cogen +import org.scalacheck.Gen + +trait ArbitraryBList { + def bListGen[A](implicit arb: Arbitrary[A], cogen: Cogen[A]): Gen[BList[A]] = { + // implicit lazy val arbBList: Arbitrary[BList[A]] = arbitraryBList[A] + + Gen.oneOf( + // empty + Gen.const(BList.empty), + // from List + // arbitrary[List[A]].map(BList.fromList(_)), + // prepend + Gen.delay(for { + hs <- arbitrary[BList[A]] + a <- arbitrary[A] + } yield hs.prepend(a)), + // concat + Gen.delay(for { + ls <- arbitrary[BList[A]] + xs <- arbitrary[BList[A]] + } yield ls.concat(xs)), + // map + Gen.delay(for { + l <- arbitrary[BList[A]] + fn <- arbitrary[A => A] + } yield l.map(fn)) + ) + } + + implicit def arbitraryBList[A](implicit arb: Arbitrary[A], cogen: Cogen[A]): Arbitrary[BList[A]] = + Arbitrary(bListGen(arb, cogen)) +} + +object ArbitraryBList extends ArbitraryBList diff --git a/scalacheck/src/main/scala/cats/collections/arbitrary/all.scala b/scalacheck/src/main/scala/cats/collections/arbitrary/all.scala index f539e0c4..79b699b6 100644 --- a/scalacheck/src/main/scala/cats/collections/arbitrary/all.scala +++ b/scalacheck/src/main/scala/cats/collections/arbitrary/all.scala @@ -27,4 +27,6 @@ trait AllArbitrary with ArbitraryMap with ArbitraryHashMap with ArbitraryPredicate + with ArbitraryTreeList + with ArbitraryBList with CogenInstances diff --git a/scalacheck/src/main/scala/cats/collections/arbitrary/package.scala b/scalacheck/src/main/scala/cats/collections/arbitrary/package.scala index df327980..9017b6b2 100644 --- a/scalacheck/src/main/scala/cats/collections/arbitrary/package.scala +++ b/scalacheck/src/main/scala/cats/collections/arbitrary/package.scala @@ -29,5 +29,7 @@ package object arbitrary { object map extends ArbitraryMap object hashmap extends ArbitraryHashMap object predicate extends ArbitraryPredicate + object blist extends ArbitraryBList object cogen extends CogenInstances + } diff --git a/tests/src/test/scala/cats/collections/BListSuite.scala b/tests/src/test/scala/cats/collections/BListSuite.scala index a9c07c7d..2892d4fe 100644 --- a/tests/src/test/scala/cats/collections/BListSuite.scala +++ b/tests/src/test/scala/cats/collections/BListSuite.scala @@ -21,15 +21,18 @@ package cats.collections +//import cats.syntax.all._ +import cats.collections.arbitrary.blist._ +//import cats.laws.discipline._ +import cats.Eq import munit.DisciplineSuite +import org.scalacheck.Prop._ import org.scalacheck.Test +//import org.scalacheck.{Arbitrary, Cogen, Gen, Test} import scala.math.pow -object global { - val BlockSize: Int = 4 -} - class BListSuite extends DisciplineSuite { + override def scalaCheckTestParameters: Test.Parameters = DefaultScalaCheckPropertyCheckConfig.default @@ -53,7 +56,7 @@ class BListSuite extends DisciplineSuite { val n = m.concat(l) n.uncons match { - case None => fail("should not be empty") + // case None => fail("should not be empty") case Some((h, t)) => assertEquals(h, m.head) assertEquals(t.toString, l.toString) @@ -62,11 +65,11 @@ class BListSuite extends DisciplineSuite { test("prepend when block is full") { var l = BList.empty[Int] - for (i <- (0 until global.BlockSize).reverse) { + for (i <- (0 until BList.BlockSize).reverse) { l = l.prepend(i) } val m = l.prepend(-1) - assertEquals(m.toList, (-1 until global.BlockSize).toList) + assertEquals(m.toList, (-1 until BList.BlockSize).toList) } test("headOption on empty list") { @@ -92,12 +95,12 @@ class BListSuite extends DisciplineSuite { test("get/getUnsafe on a Blist with incomplete blocks") { var l = BList.empty[Int] var m = BList.empty[Int] - for (i <- (0 until global.BlockSize).reverse) { + for (i <- (0 until BList.BlockSize).reverse) { l = l.prepend(i) m = l.concat(m) } - assertEquals(m.getUnsafe((global.BlockSize + (global.BlockSize - 1)).toLong), 2) - assertEquals(m.get((global.BlockSize + (global.BlockSize - 1)).toLong), Some(2)) + assertEquals(m.getUnsafe((BList.BlockSize + (BList.BlockSize - 1)).toLong), 2) + assertEquals(m.get((BList.BlockSize + (BList.BlockSize - 1)).toLong), Some(2)) } test("get/getUnsafe index too small") { val l = BList.empty[Int].prepend(5).prepend(4).prepend(3).prepend(2).prepend(1) @@ -112,26 +115,26 @@ class BListSuite extends DisciplineSuite { test("get/getUnsafe index too big") { var l = BList.empty[Int] var m = BList.empty[Int] - for (i <- (0 until global.BlockSize).reverse) { + for (i <- (0 until BList.BlockSize).reverse) { l = l.prepend(i) m = l.concat(m) } - // size is triangle numbers in blocksize. size should be n(n+1)/2 where n is blocksize - // so blocksize ** 2 + 1 will always be out of bounds - val idx: Long = pow(global.BlockSize.toDouble, 2.0).toLong + 1L + // size is triangle numbers in BList.BlockSize. size should be n(n+1)/2 where n is BList.BlockSize + // so BList.BlockSize ** 2 + 1 will always be out of bounds + val idx: Long = pow(BList.BlockSize.toDouble, 2.0).toLong + 1L assertEquals(m.get(idx), None) intercept[IndexOutOfBoundsException](m.getUnsafe(idx)) } test("lastOption with incomplete blocks") { var l = BList.empty[Int] var m = BList.empty[Int] - for (i <- (0 until global.BlockSize).reverse) { + for (i <- (0 until BList.BlockSize).reverse) { l = l.prepend(i) m = l.concat(m) } // last should be blocksize-1 - assertEquals(m.lastOption, Some(global.BlockSize - 1)) + assertEquals(m.lastOption, Some(BList.BlockSize - 1)) } test("lastOption on empty list") { val l = BList.empty[Int] @@ -140,12 +143,12 @@ class BListSuite extends DisciplineSuite { test("size on a triangle number construction") { var l = BList.empty[Int] var m = BList.empty[Int] - for (i <- (0 until global.BlockSize).reverse) { + for (i <- (0 until BList.BlockSize).reverse) { l = l.prepend(i) m = l.concat(m) } - // size is triangle numbers in blocksize. formula: n(n+1)/2 - val expected: Long = (global.BlockSize * (global.BlockSize + 1)).toLong / 2L + // size is triangle numbers in BList.BlockSize. formula: n(n+1)/2 + val expected: Long = (BList.BlockSize * (BList.BlockSize + 1)).toLong / 2L assertEquals(m.size, expected) } test("size on empty") { @@ -158,7 +161,7 @@ class BListSuite extends DisciplineSuite { val m = l.prepend(0) m.uncons match { - case None => fail("should not be empty") + // case None => fail("should not be empty") case Some((h, t)) => assertEquals(h, 0) assertEquals(t.toString, l.toString) @@ -172,7 +175,6 @@ class BListSuite extends DisciplineSuite { l = l.prepend(i) m = l.concat(m) } - val mapped = m.map(_ + 10) assertEquals(mapped.getUnsafe(20L), m.getUnsafe(20L) + 10) assertEquals(mapped.getUnsafe(25L), m.getUnsafe(25L) + 10) @@ -240,4 +242,92 @@ class BListSuite extends DisciplineSuite { assertEquals(m.drop(-10).toString, m.toString) assertEquals(m.drop(-200).toString, m.toString) } + + private def testHomomorphism[A, B: Eq](as: BList[A])(fn: BList[A] => B, gn: List[A] => B): Unit = { + val la = as.toList + assert(Eq[B].eqv(fn(as), gn(la))) + } + + property("size works")(forAll { (xs: BList[Int]) => + testHomomorphism(xs)({ _.size }, { _.size.toLong }) + }) + property("last is the same as get(size-1)")(forAll { (xs: BList[Int]) => + assertEquals(xs.lastOption, xs.get(xs.size - 1L)) + }) + property("concat works")(forAll { (xs: BList[Int], ys: BList[Int]) => + testHomomorphism(xs)({ l => l.concat(ys).toList }, { _ ++ ys.toList }) + }) + + property("get and getUnsafe are consistent")(forAll { (xs: BList[Int]) => + assertEquals(xs.get(-1L), None) + assertEquals(xs.get(xs.size), None) + intercept[IndexOutOfBoundsException](xs.getUnsafe(-1L)) + intercept[IndexOutOfBoundsException](xs.getUnsafe(xs.size)) + + val list = xs.toList + (0L until xs.size).foreach { idx => + assertEquals(xs.get(idx), Some(list(idx.toInt))) + assertEquals(xs.getUnsafe(idx), list(idx.toInt)) + } + }) + + property("prepending head on to tail makes the same list")(forAll { (xs: BList[Int]) => + xs.headOption match { + case Some(h) => + xs.tailOption match { + case Some(t) => assertEquals(t.prepend(h).toString, xs.toString) + case None => assertEquals(BList.empty.prepend(h).toString, xs.toString) + } + case None => assertEquals(xs, BList.empty) + } + }) + + property("headoption, head consistent and tailoption, tail consistent")(forAll { (xs: BList[Int]) => + xs.headOption match { + case Some(h) => assertEquals(h, xs.asInstanceOf[BList.NonEmpty[Int]].head) + case None => // head wont work because xs is the empty list + } + xs.tailOption match { + case Some(t) => assertEquals(t, xs.asInstanceOf[BList.NonEmpty[Int]].tail) + case None => // tail wont work because xs is the empty list + } + }) + + property("headOption works")(forAll { (xs: BList[Int]) => + testHomomorphism(xs)({ _.headOption }, { _.headOption }) + }) + + property("lastOption works")(forAll { (xs: BList[Int]) => + testHomomorphism(xs)({ _.lastOption }, { _.lastOption }) + }) + + property("toList inverse of fromList")(forAll { (xs: BList[Int]) => + assertEquals(xs.toString, BList.fromList(xs.toList).toString) + }) + + property("pattern matching works")(forAll { (xs: BList[Int]) => + xs match { + case BList.NonEmpty(h, t) => + assertEquals(xs.headOption, Option(h)) + assertEquals(xs.tailOption, Option(t)) + case BList.Empty => + assertEquals(xs, BList.Empty) + assertEquals(xs.uncons, None) + assertEquals(xs.headOption, None) + assertEquals(xs.tailOption, None) + assertEquals(xs.lastOption, None) + } + }) + + property("apply/unapply are inverses for NonEmpty")(forAll { (head: Int, tail: BList[Int]) => + BList.NonEmpty(head, tail) match { + case BList.NonEmpty(h, t) => + assertEquals(h, head) + assertEquals(t.toString, tail.toString) + } + }) + + property("fromListReverse == .reverse fromList")(forAll { (xs: List[Int]) => + assertEquals(BList.fromListReverse(xs).toString, BList.fromList(xs.reverse).toString) + }) } From dba9c9a3f2503d7d565a47db8003b79f363f638f Mon Sep 17 00:00:00 2001 From: Lola Denney Date: Thu, 11 Jun 2026 12:01:12 -0400 Subject: [PATCH 18/36] trying to get past ci again --- core/src/main/scala/cats/collections/BList.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/scala/cats/collections/BList.scala b/core/src/main/scala/cats/collections/BList.scala index f8eaea2a..f37c49b9 100644 --- a/core/src/main/scala/cats/collections/BList.scala +++ b/core/src/main/scala/cats/collections/BList.scala @@ -69,7 +69,7 @@ object BList { final private[collections] val BlockSize = 4 // test with different values case object Empty extends BList[Nothing] { - def unapply[A](l: Empty.type): None.type = None + //def unapply[A](l: Empty.type): None.type = None def uncons = None def prepend[B >: Nothing](a: B): BList.NonEmpty[B] = { // why would we put the element at the end of a block? dont we want to fill from start out. in this case we would want head and tail pointer val ary = new Array[Any](BlockSize) From 34a91f2684c33a04cb44b6df534644956f78dd4d Mon Sep 17 00:00:00 2001 From: Lola Denney Date: Thu, 11 Jun 2026 12:07:37 -0400 Subject: [PATCH 19/36] banging my head against the ci wall --- core/src/main/scala/cats/collections/BList.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/scala/cats/collections/BList.scala b/core/src/main/scala/cats/collections/BList.scala index f37c49b9..ead4bb56 100644 --- a/core/src/main/scala/cats/collections/BList.scala +++ b/core/src/main/scala/cats/collections/BList.scala @@ -69,7 +69,7 @@ object BList { final private[collections] val BlockSize = 4 // test with different values case object Empty extends BList[Nothing] { - //def unapply[A](l: Empty.type): None.type = None + // def unapply[A](l: Empty.type): None.type = None def uncons = None def prepend[B >: Nothing](a: B): BList.NonEmpty[B] = { // why would we put the element at the end of a block? dont we want to fill from start out. in this case we would want head and tail pointer val ary = new Array[Any](BlockSize) From 62c530e0a2acb3b3aabb51722c421f95933c6138 Mon Sep 17 00:00:00 2001 From: Lola Denney Date: Thu, 11 Jun 2026 13:51:03 -0400 Subject: [PATCH 20/36] stack overflow in blist generation for property tests addressed --- .../main/scala/cats/collections/BList.scala | 1 + .../arbitrary/ArbitraryBList.scala | 45 +++++++++---------- .../scala/cats/collections/BListSuite.scala | 2 + 3 files changed, 25 insertions(+), 23 deletions(-) diff --git a/core/src/main/scala/cats/collections/BList.scala b/core/src/main/scala/cats/collections/BList.scala index ead4bb56..35d0e20e 100644 --- a/core/src/main/scala/cats/collections/BList.scala +++ b/core/src/main/scala/cats/collections/BList.scala @@ -262,6 +262,7 @@ object BList { 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)) + // case Impl(_,_,_) => Impl(self.offset, self.block.asInstanceOf[Array[B]], go(self.tailBList.asInstanceOf[Impl[A]])) } } diff --git a/scalacheck/src/main/scala/cats/collections/arbitrary/ArbitraryBList.scala b/scalacheck/src/main/scala/cats/collections/arbitrary/ArbitraryBList.scala index 720e299b..81ca3e29 100644 --- a/scalacheck/src/main/scala/cats/collections/arbitrary/ArbitraryBList.scala +++ b/scalacheck/src/main/scala/cats/collections/arbitrary/ArbitraryBList.scala @@ -29,29 +29,28 @@ import org.scalacheck.Gen trait ArbitraryBList { def bListGen[A](implicit arb: Arbitrary[A], cogen: Cogen[A]): Gen[BList[A]] = { - // implicit lazy val arbBList: Arbitrary[BList[A]] = arbitraryBList[A] - - Gen.oneOf( - // empty - Gen.const(BList.empty), - // from List - // arbitrary[List[A]].map(BList.fromList(_)), - // prepend - Gen.delay(for { - hs <- arbitrary[BList[A]] - a <- arbitrary[A] - } yield hs.prepend(a)), - // concat - Gen.delay(for { - ls <- arbitrary[BList[A]] - xs <- arbitrary[BList[A]] - } yield ls.concat(xs)), - // map - Gen.delay(for { - l <- arbitrary[BList[A]] - fn <- arbitrary[A => A] - } yield l.map(fn)) - ) + Gen.sized { + case 0 => Gen.const(BList.empty) + case n => + Gen.oneOf( + // empty + Gen.const(BList.empty), + // prepend + for { + hs <- Gen.resize(n / 2, bListGen[A]) + a <- arbitrary[A] + } yield hs.prepend(a), + // concat + for { + xs <- Gen.resize(n / 4, bListGen[A]) + ys <- Gen.resize(n / 4, bListGen[A]) + } yield xs.concat(ys), + // map + Gen.resize(n / 2, bListGen[A]).flatMap { l => + arbitrary[A => A].map(fn => l.map(fn)) + } + ) + } } implicit def arbitraryBList[A](implicit arb: Arbitrary[A], cogen: Cogen[A]): Arbitrary[BList[A]] = diff --git a/tests/src/test/scala/cats/collections/BListSuite.scala b/tests/src/test/scala/cats/collections/BListSuite.scala index 2892d4fe..e796b721 100644 --- a/tests/src/test/scala/cats/collections/BListSuite.scala +++ b/tests/src/test/scala/cats/collections/BListSuite.scala @@ -32,6 +32,7 @@ import org.scalacheck.Test import scala.math.pow class BListSuite extends DisciplineSuite { + // override def scalaCheckInitialSeed = "kueY5dMKVWrkQrfGXwqy_Wh4bK4-qnfvz_2D0-LrwPI=" override def scalaCheckTestParameters: Test.Parameters = DefaultScalaCheckPropertyCheckConfig.default @@ -254,6 +255,7 @@ class BListSuite extends DisciplineSuite { property("last is the same as get(size-1)")(forAll { (xs: BList[Int]) => assertEquals(xs.lastOption, xs.get(xs.size - 1L)) }) + property("concat works")(forAll { (xs: BList[Int], ys: BList[Int]) => testHomomorphism(xs)({ l => l.concat(ys).toList }, { _ ++ ys.toList }) }) From d67a55c64241c3d5d842b26ac3eaa35ea301a4e4 Mon Sep 17 00:00:00 2001 From: Lola Denney Date: Fri, 12 Jun 2026 09:44:30 -0400 Subject: [PATCH 21/36] fixed uncons and tail to prevent blocking GC --- core/src/main/scala/cats/collections/BList.scala | 14 ++++++++------ .../test/scala/cats/collections/BListSuite.scala | 4 ++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/core/src/main/scala/cats/collections/BList.scala b/core/src/main/scala/cats/collections/BList.scala index 35d0e20e..be1b89b2 100644 --- a/core/src/main/scala/cats/collections/BList.scala +++ b/core/src/main/scala/cats/collections/BList.scala @@ -98,6 +98,8 @@ object BList { 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]] } @@ -118,9 +120,7 @@ object BList { // (maybe impl will be covariant or not) private case class Impl[A](offset: Int, block: Array[A], tailBList: BList[A]) extends NonEmpty[A] { def uncons: Some[(A, BList[A])] = { - val nextOffset = offset + 1 - val next = if (nextOffset == block.length) tailBList else Impl(nextOffset, block, tailBList) - Some((block(offset), next)) + Some((block(offset), this.tail)) } def prepend[B >: A](a: B): BList.NonEmpty[B] = { if (offset > 0) { @@ -140,15 +140,17 @@ object BList { } def tail: BList[A] = { if (offset < BlockSize - 1) { - Impl(offset + 1, block, tailBList) + 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: Option[A] = { + def headOption: Some[A] = { Some(block(offset)) } - def tailOption: Option[BList[A]] = { + def tailOption: Some[BList[A]] = { Some(tail) } def get(idx: Long): Option[A] = { diff --git a/tests/src/test/scala/cats/collections/BListSuite.scala b/tests/src/test/scala/cats/collections/BListSuite.scala index e796b721..c8019a05 100644 --- a/tests/src/test/scala/cats/collections/BListSuite.scala +++ b/tests/src/test/scala/cats/collections/BListSuite.scala @@ -290,7 +290,7 @@ class BListSuite extends DisciplineSuite { case None => // head wont work because xs is the empty list } xs.tailOption match { - case Some(t) => assertEquals(t, xs.asInstanceOf[BList.NonEmpty[Int]].tail) + case Some(t) => assertEquals(t.toString, xs.asInstanceOf[BList.NonEmpty[Int]].tail.toString) case None => // tail wont work because xs is the empty list } }) @@ -311,7 +311,7 @@ class BListSuite extends DisciplineSuite { xs match { case BList.NonEmpty(h, t) => assertEquals(xs.headOption, Option(h)) - assertEquals(xs.tailOption, Option(t)) + assertEquals(xs.asInstanceOf[BList.NonEmpty[Int]].tail.toString, t.toString) case BList.Empty => assertEquals(xs, BList.Empty) assertEquals(xs.uncons, None) From f8a1d26a7f907782c814ad09790bc00ae6ed2d1c Mon Sep 17 00:00:00 2001 From: Lola Denney Date: Fri, 12 Jun 2026 10:05:53 -0400 Subject: [PATCH 22/36] make Impl class, removed case --- .../main/scala/cats/collections/BList.scala | 66 ++++++++++--------- 1 file changed, 36 insertions(+), 30 deletions(-) diff --git a/core/src/main/scala/cats/collections/BList.scala b/core/src/main/scala/cats/collections/BList.scala index be1b89b2..0c8c1781 100644 --- a/core/src/main/scala/cats/collections/BList.scala +++ b/core/src/main/scala/cats/collections/BList.scala @@ -115,10 +115,16 @@ object BList { 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)) } // (maybe impl will be covariant or not) - private case class Impl[A](offset: Int, block: Array[A], tailBList: BList[A]) extends NonEmpty[A] { + private class Impl[A](val offset: Int, val block: Array[A], val tailBList: BList[A]) extends NonEmpty[A] { + // override def equals(that: Any): Boolean = + //todo + //override def hashCode(): Int = + //todo def uncons: Some[(A, BList[A])] = { Some((block(offset), this.tail)) } @@ -160,11 +166,11 @@ object BList { def go(idx: Long, l: BList[A]): Option[A] = { l match { case Empty => None - case Impl(offset, block, tailBList) => - if (idx < BlockSize - offset) { - Some(block(offset + idx.toInt)) + case Impl(offset1, block1, tailBList1) => + if (idx < BlockSize - offset1) { + Some(block1(offset1 + idx.toInt)) } else { - go(idx - (BlockSize - offset), tailBList) + go(idx - (BlockSize - offset1), tailBList1) } } } @@ -179,11 +185,11 @@ object BList { def go(idx: Long, l: BList[A]): A = { l match { case Empty => throw new IndexOutOfBoundsException - case Impl(offset, block, tailBList) => - if (idx < BlockSize - offset) { - block(offset + idx.toInt) + case Impl(offset1, block1, tailBList1) => + if (idx < BlockSize - offset1) { + block1(offset1 + idx.toInt) } else { - go(idx - (BlockSize - offset), tailBList) + go(idx - (BlockSize - offset1), tailBList1) } } } @@ -204,7 +210,7 @@ object BList { def loop(l: BList[A], acc: Long): Long = { l match { case Empty => acc - case Impl(offset, _, tailBList) => loop(tailBList, acc + (BlockSize - offset)) + case Impl(offset1, _, tailBList1) => loop(tailBList1, acc + (BlockSize - offset1)) } } loop(this, 0L) @@ -223,14 +229,14 @@ object BList { def loop(acc: B, l: BList[A]): B = l match { case Empty => acc - case Impl(offset, block, tailBList) => + case Impl(offset1, block1, tailBList1) => var newacc = acc - var i = offset - while (i < block.length) { - newacc = fn(newacc, block(i)) + var i = offset1 + while (i < block1.length) { + newacc = fn(newacc, block1(i)) i += 1 } - loop(newacc, tailBList) + loop(newacc, tailBList1) } loop(acc, this) } @@ -243,14 +249,14 @@ object BList { l match { case Empty => Empty - case Impl(offset, block, tailBList) => - if (n >= BlockSize - offset) { - go(n - (BlockSize - offset), tailBList) + case Impl(offset1, block1, tailBList1) => + if (n >= BlockSize - offset1) { + go(n - (BlockSize - offset1), tailBList1) } else { val ary = new Array[Any](BlockSize) - val newOffset: Int = offset + n.asInstanceOf[Int] // type conversion safe because 0 builder.result() - case Impl(offset, block, tailBList) => + case Impl(offset1, block1, tailBList1) => // append valid things in the block to acc - for (i <- offset until BlockSize) { - builder += block(i) + for (i <- offset1 until BlockSize) { + builder += block1(i) } - loop(tailBList) + loop(tailBList1) } loop(this) } @@ -299,15 +305,15 @@ object BList { if (!first) strb.append(", "): Unit l match { case Empty => strb.append("Empty"): Unit - case Impl(offset, block, tailBList) => + case Impl(offset1, block1, tailBList1) => strb.append("Block(") - strb.append(block(offset).toString) - for (i <- offset + 1 until BlockSize) { + strb.append(block1(offset1).toString) + for (i <- offset1 + 1 until BlockSize) { strb.append(", ") - strb.append(block(i).toString) + strb.append(block1(i).toString) } strb.append(")") - loop(false, tailBList) + loop(false, tailBList1) } } loop(true, this) From 2e56cfb973303c9ca22836b07d44c9176a3301a0 Mon Sep 17 00:00:00 2001 From: Lola Denney Date: Mon, 15 Jun 2026 10:00:02 -0400 Subject: [PATCH 23/36] overrode equals and hashcode --- .../main/scala/cats/collections/BList.scala | 72 +++++++++++++++---- .../scala/cats/collections/BListSuite.scala | 32 ++++----- 2 files changed, 76 insertions(+), 28 deletions(-) diff --git a/core/src/main/scala/cats/collections/BList.scala b/core/src/main/scala/cats/collections/BList.scala index 0c8c1781..3459a6e2 100644 --- a/core/src/main/scala/cats/collections/BList.scala +++ b/core/src/main/scala/cats/collections/BList.scala @@ -37,6 +37,7 @@ sealed abstract class BList[+A] { def drop(n: Long): BList[A] def concat[B >: A](l2: BList[B]): BList[B] def toList: List[A] + def isEmpty: Boolean // for development and testing private[collections] def toStringInBlocks: String @@ -88,6 +89,7 @@ object BList { 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 private[collections] def toStringInBlocks: String = "Empty" } @@ -115,16 +117,61 @@ object BList { 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)) + def unapply[A](l: Impl[A]): Some[(Int, Array[A], BList[A])] = + Some((l.offset, l.block, l.tailBList)) } // (maybe impl will be covariant or not) private class Impl[A](val offset: Int, val block: Array[A], val tailBList: BList[A]) extends NonEmpty[A] { - // override def equals(that: Any): Boolean = - //todo - //override def hashCode(): Int = - //todo + + override def equals(other: Any): Boolean = { + // helper to get next element of BList + def next[B](node: Impl[B], curoffset: Int): Option[(B, Impl[B], Int)] = { + if (curoffset < BlockSize - 1) { + Some((node.block(curoffset + 1), node, curoffset + 1)) + } else { + // we need to see if the tail is empty and try to take an element from it + node.tailBList match { + case Empty => None + case Impl(tailoffset, tailblock, _) => // there will be at least one element in every block + Some((tailblock(tailoffset), node.tailBList.asInstanceOf[Impl[B]], tailoffset + 1)) + } + } + } + + other match { + case that: Impl[_] => + // 3-tuple has structure: current element, current node (nonempty), current offset in to curnode's block + @tailrec + def loop[B](list1: (A, Impl[A], Int), list2: (B, Impl[B], Int)): Boolean = { + if (list1._1 != list2._1) false + + (next(list1._2, list1._3), next(list2._2, list2._3)) match { + case (None, None) => true + case (None, _) | (_, None) => false + case (Some(list1_tail), Some(list2_tail)) => + loop(list1_tail, list2_tail) + } + } + + loop( + (this.block(this.offset), this, this.offset), // list1 + (that.block(that.offset), that, that.offset) // list2 + ) + + case _ => false + } + } + + override def hashCode(): Int = { + // determined by only the valid elements in the first block + var res = 31 + for (i <- this.offset until BlockSize) { + res = res * 31 + this.block(i).hashCode() + } + res + } + def uncons: Some[(A, BList[A])] = { Some((block(offset), this.tail)) } @@ -165,7 +212,7 @@ object BList { @tailrec def go(idx: Long, l: BList[A]): Option[A] = { l match { - case Empty => None + case Empty => None case Impl(offset1, block1, tailBList1) => if (idx < BlockSize - offset1) { Some(block1(offset1 + idx.toInt)) @@ -184,7 +231,7 @@ object BList { @tailrec def go(idx: Long, l: BList[A]): A = { l match { - case Empty => throw new IndexOutOfBoundsException + case Empty => throw new IndexOutOfBoundsException case Impl(offset1, block1, tailBList1) => if (idx < BlockSize - offset1) { block1(offset1 + idx.toInt) @@ -209,7 +256,7 @@ object BList { @tailrec def loop(l: BList[A], acc: Long): Long = { l match { - case Empty => acc + case Empty => acc case Impl(offset1, _, tailBList1) => loop(tailBList1, acc + (BlockSize - offset1)) } } @@ -228,7 +275,7 @@ object BList { @tailrec def loop(acc: B, l: BList[A]): B = l match { - case Empty => acc + case Empty => acc case Impl(offset1, block1, tailBList1) => var newacc = acc var i = offset1 @@ -286,7 +333,7 @@ object BList { @tailrec def loop(l: BList[A]): List[A] = l match { - case Empty => builder.result() + case Empty => builder.result() case Impl(offset1, block1, tailBList1) => // append valid things in the block to acc for (i <- offset1 until BlockSize) { @@ -296,6 +343,7 @@ object BList { } loop(this) } + def isEmpty: Boolean = false private[collections] def toStringInBlocks: String = { val strb = new java.lang.StringBuilder @@ -304,7 +352,7 @@ object BList { def loop(first: Boolean, l: BList[A]): Unit = { if (!first) strb.append(", "): Unit l match { - case Empty => strb.append("Empty"): Unit + case Empty => strb.append("Empty"): Unit case Impl(offset1, block1, tailBList1) => strb.append("Block(") strb.append(block1(offset1).toString) diff --git a/tests/src/test/scala/cats/collections/BListSuite.scala b/tests/src/test/scala/cats/collections/BListSuite.scala index c8019a05..cf6f732c 100644 --- a/tests/src/test/scala/cats/collections/BListSuite.scala +++ b/tests/src/test/scala/cats/collections/BListSuite.scala @@ -47,7 +47,7 @@ class BListSuite extends DisciplineSuite { val n1 = m1.concat(l1) val n2 = m2.concat(l2) - assertEquals(n1.toString, n2.toString) + assertEquals(n1, n2) } test("uncons when tail is a new node") { @@ -60,7 +60,7 @@ class BListSuite extends DisciplineSuite { // case None => fail("should not be empty") case Some((h, t)) => assertEquals(h, m.head) - assertEquals(t.toString, l.toString) + assertEquals(t, l) } } @@ -165,7 +165,7 @@ class BListSuite extends DisciplineSuite { // case None => fail("should not be empty") case Some((h, t)) => assertEquals(h, 0) - assertEquals(t.toString, l.toString) + assertEquals(t, l) } } @@ -227,9 +227,9 @@ class BListSuite extends DisciplineSuite { l = l.prepend(i) m = l.concat(m) } - assertEquals(m.drop(10).toString, BList.empty[Int].toString) - assertEquals(m.drop(20).toString, BList.empty[Int].toString) - assertEquals(m.drop(1000).toString, BList.empty[Int].toString) + assertEquals(m.drop(10), BList.empty[Int]) + assertEquals(m.drop(20), BList.empty[Int]) + assertEquals(m.drop(1000), BList.empty[Int]) } test("drop nothing") { var l = BList.empty[Int] @@ -239,9 +239,9 @@ class BListSuite extends DisciplineSuite { m = l.concat(m) } - assertEquals(m.drop(0).toString, m.toString) - assertEquals(m.drop(-10).toString, m.toString) - assertEquals(m.drop(-200).toString, m.toString) + assertEquals(m.drop(0), m) + assertEquals(m.drop(-10), m) + assertEquals(m.drop(-200), m) } private def testHomomorphism[A, B: Eq](as: BList[A])(fn: BList[A] => B, gn: List[A] => B): Unit = { @@ -277,8 +277,8 @@ class BListSuite extends DisciplineSuite { xs.headOption match { case Some(h) => xs.tailOption match { - case Some(t) => assertEquals(t.prepend(h).toString, xs.toString) - case None => assertEquals(BList.empty.prepend(h).toString, xs.toString) + case Some(t) => assertEquals(t.prepend(h), xs) + case None => assertEquals(BList.empty.prepend(h), xs) } case None => assertEquals(xs, BList.empty) } @@ -290,7 +290,7 @@ class BListSuite extends DisciplineSuite { case None => // head wont work because xs is the empty list } xs.tailOption match { - case Some(t) => assertEquals(t.toString, xs.asInstanceOf[BList.NonEmpty[Int]].tail.toString) + case Some(t) => assertEquals(t, xs.asInstanceOf[BList.NonEmpty[Int]].tail) case None => // tail wont work because xs is the empty list } }) @@ -304,14 +304,14 @@ class BListSuite extends DisciplineSuite { }) property("toList inverse of fromList")(forAll { (xs: BList[Int]) => - assertEquals(xs.toString, BList.fromList(xs.toList).toString) + assertEquals(xs, BList.fromList(xs.toList)) }) property("pattern matching works")(forAll { (xs: BList[Int]) => xs match { case BList.NonEmpty(h, t) => assertEquals(xs.headOption, Option(h)) - assertEquals(xs.asInstanceOf[BList.NonEmpty[Int]].tail.toString, t.toString) + assertEquals(xs.asInstanceOf[BList.NonEmpty[Int]].tail, t) case BList.Empty => assertEquals(xs, BList.Empty) assertEquals(xs.uncons, None) @@ -325,11 +325,11 @@ class BListSuite extends DisciplineSuite { BList.NonEmpty(head, tail) match { case BList.NonEmpty(h, t) => assertEquals(h, head) - assertEquals(t.toString, tail.toString) + assertEquals(t, tail) } }) property("fromListReverse == .reverse fromList")(forAll { (xs: List[Int]) => - assertEquals(BList.fromListReverse(xs).toString, BList.fromList(xs.reverse).toString) + assertEquals(BList.fromListReverse(xs), BList.fromList(xs.reverse)) }) } From d623b834086aaa2e533ddcda2c592aac6f445bed Mon Sep 17 00:00:00 2001 From: Lola Denney Date: Mon, 15 Jun 2026 11:12:19 -0400 Subject: [PATCH 24/36] fixed for scala 3 --- .../main/scala/cats/collections/BList.scala | 93 ++++++++++--------- 1 file changed, 49 insertions(+), 44 deletions(-) diff --git a/core/src/main/scala/cats/collections/BList.scala b/core/src/main/scala/cats/collections/BList.scala index 3459a6e2..7c0bfeb0 100644 --- a/core/src/main/scala/cats/collections/BList.scala +++ b/core/src/main/scala/cats/collections/BList.scala @@ -132,9 +132,9 @@ object BList { } else { // we need to see if the tail is empty and try to take an element from it node.tailBList match { - case Empty => None - case Impl(tailoffset, tailblock, _) => // there will be at least one element in every block - Some((tailblock(tailoffset), node.tailBList.asInstanceOf[Impl[B]], tailoffset + 1)) + case Empty => None + case tail: Impl[B] @unchecked => // there will be at least one element in every block + Some((tail.block(tail.offset), tail, tail.offset + 1)) } } } @@ -144,14 +144,19 @@ object BList { // 3-tuple has structure: current element, current node (nonempty), current offset in to curnode's block @tailrec def loop[B](list1: (A, Impl[A], Int), list2: (B, Impl[B], Int)): Boolean = { - if (list1._1 != list2._1) false + if (list1._1 != list2._1) { + false + } else { + + (next(list1._2, list1._3), next(list2._2, list2._3)) match { + case (None, None) => true + case (None, _) | (_, None) => false + case (Some(list1_tail), Some(list2_tail)) => + loop(list1_tail, list2_tail) + } - (next(list1._2, list1._3), next(list2._2, list2._3)) match { - case (None, None) => true - case (None, _) | (_, None) => false - case (Some(list1_tail), Some(list2_tail)) => - loop(list1_tail, list2_tail) } + } loop( @@ -212,12 +217,12 @@ object BList { @tailrec def go(idx: Long, l: BList[A]): Option[A] = { l match { - case Empty => None - case Impl(offset1, block1, tailBList1) => - if (idx < BlockSize - offset1) { - Some(block1(offset1 + idx.toInt)) + case Empty => None + case impl: Impl[A] @unchecked => + if (idx < BlockSize - impl.offset) { + Some(impl.block(impl.offset + idx.toInt)) } else { - go(idx - (BlockSize - offset1), tailBList1) + go(idx - (BlockSize - impl.offset), impl.tailBList) } } } @@ -231,12 +236,12 @@ object BList { @tailrec def go(idx: Long, l: BList[A]): A = { l match { - case Empty => throw new IndexOutOfBoundsException - case Impl(offset1, block1, tailBList1) => - if (idx < BlockSize - offset1) { - block1(offset1 + idx.toInt) + 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 - offset1), tailBList1) + go(idx - (BlockSize - impl.offset), impl.tailBList) } } } @@ -256,8 +261,8 @@ object BList { @tailrec def loop(l: BList[A], acc: Long): Long = { l match { - case Empty => acc - case Impl(offset1, _, tailBList1) => loop(tailBList1, acc + (BlockSize - offset1)) + case Empty => acc + case impl: Impl[A] @unchecked => loop(impl.tailBList, acc + (BlockSize - impl.offset)) } } loop(this, 0L) @@ -275,15 +280,15 @@ object BList { @tailrec def loop(acc: B, l: BList[A]): B = l match { - case Empty => acc - case Impl(offset1, block1, tailBList1) => + case Empty => acc + case impl: Impl[A] @unchecked => var newacc = acc - var i = offset1 - while (i < block1.length) { - newacc = fn(newacc, block1(i)) + var i = impl.offset + while (i < impl.block.length) { + newacc = fn(newacc, impl.block(i)) i += 1 } - loop(newacc, tailBList1) + loop(newacc, impl.tailBList) } loop(acc, this) } @@ -296,14 +301,14 @@ object BList { l match { case Empty => Empty - case Impl(offset1, block1, tailBList1) => - if (n >= BlockSize - offset1) { - go(n - (BlockSize - offset1), tailBList1) + 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 = offset1 + n.asInstanceOf[Int] // type conversion safe because 0 builder.result() - case Impl(offset1, block1, tailBList1) => + case Empty => builder.result() + case impl: Impl[A] @unchecked => // append valid things in the block to acc - for (i <- offset1 until BlockSize) { - builder += block1(i) + for (i <- impl.offset until BlockSize) { + builder += impl.block(i) } - loop(tailBList1) + loop(impl.tailBList) } loop(this) } @@ -352,16 +357,16 @@ object BList { def loop(first: Boolean, l: BList[A]): Unit = { if (!first) strb.append(", "): Unit l match { - case Empty => strb.append("Empty"): Unit - case Impl(offset1, block1, tailBList1) => + case Empty => strb.append("Empty"): Unit + case impl: Impl[A] @unchecked => strb.append("Block(") - strb.append(block1(offset1).toString) - for (i <- offset1 + 1 until BlockSize) { + strb.append(impl.block(impl.offset).toString) + for (i <- impl.offset + 1 until BlockSize) { strb.append(", ") - strb.append(block1(i).toString) + strb.append(impl.block(i).toString) } strb.append(")") - loop(false, tailBList1) + loop(false, impl.tailBList) } } loop(true, this) From aefdb7bd8937d75bce33259c3ceec20ae735e89f Mon Sep 17 00:00:00 2001 From: Lola Denney Date: Mon, 15 Jun 2026 12:47:18 -0400 Subject: [PATCH 25/36] fixed equals --- .../main/scala/cats/collections/BList.scala | 22 ++++++--------- .../scala/cats/collections/BListSuite.scala | 28 ++++++++++++++++++- 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/core/src/main/scala/cats/collections/BList.scala b/core/src/main/scala/cats/collections/BList.scala index 7c0bfeb0..efa9dbe2 100644 --- a/core/src/main/scala/cats/collections/BList.scala +++ b/core/src/main/scala/cats/collections/BList.scala @@ -30,7 +30,7 @@ sealed abstract class BList[+A] { def tailOption: Option[BList[A]] def get(idx: Long): Option[A] def getUnsafe(idx: Long): A - def lastOption: Option[A] // a bit more efficient than get(size-1) + 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 @@ -46,7 +46,7 @@ sealed abstract class BList[+A] { final def ++[B >: A](l2: BList[B]): BList[B] = concat(l2) - override def toString: String = { // go back and optimize with blocks + override def toString: String = { val strb = new java.lang.StringBuilder strb.append("BList(") @tailrec @@ -70,9 +70,8 @@ object BList { final private[collections] val BlockSize = 4 // test with different values case object Empty extends BList[Nothing] { - // def unapply[A](l: Empty.type): None.type = None def uncons = None - def prepend[B >: Nothing](a: B): BList.NonEmpty[B] = { // why would we put the element at the end of a block? dont we want to fill from start out. in this case we would want head and tail pointer + def prepend[B >: Nothing](a: B): BList.NonEmpty[B] = { val ary = new Array[Any](BlockSize) val offset = BlockSize - 1 ary(offset) = a @@ -94,7 +93,6 @@ object BList { } sealed abstract class NonEmpty[+A] extends BList[A] { - // TODO can put methods in here that are only safe for nonempty (ex. head, reduce) def head: A def tail: BList[A] def map[B](fn: A => B): BList.NonEmpty[B] @@ -102,7 +100,6 @@ object BList { def uncons: Some[(A, BList[A])] def headOption: Some[A] def tailOption: Some[BList[A]] - } object NonEmpty { @@ -121,7 +118,6 @@ object BList { Some((l.offset, l.block, l.tailBList)) } - // (maybe impl will be covariant or not) private class Impl[A](val offset: Int, val block: Array[A], val tailBList: BList[A]) extends NonEmpty[A] { override def equals(other: Any): Boolean = { @@ -130,18 +126,17 @@ object BList { if (curoffset < BlockSize - 1) { Some((node.block(curoffset + 1), node, curoffset + 1)) } else { - // we need to see if the tail is empty and try to take an element from it node.tailBList match { case Empty => None case tail: Impl[B] @unchecked => // there will be at least one element in every block - Some((tail.block(tail.offset), tail, tail.offset + 1)) + Some((tail.block(tail.offset), tail, tail.offset)) } } } other match { case that: Impl[_] => - // 3-tuple has structure: current element, current node (nonempty), current offset in to curnode's block + // 3-tuple has structure: element, node where it is found, offset at which it was found @tailrec def loop[B](list1: (A, Impl[A], Int), list2: (B, Impl[B], Int)): Boolean = { if (list1._1 != list2._1) { @@ -169,7 +164,7 @@ object BList { } override def hashCode(): Int = { - // determined by only the valid elements in the first block + // uses only the valid elements in the first block var res = 31 for (i <- this.offset until BlockSize) { res = res * 31 + this.block(i).hashCode() @@ -187,7 +182,7 @@ object BList { ary(nextOffset) = a Impl(nextOffset, ary, tailBList) } else { - val ary = new Array[Any](BlockSize) // need a blank one for a new block + val ary = new Array[Any](BlockSize) val offset = BlockSize - 1 ary(offset) = a Impl(offset, ary, this) @@ -322,11 +317,10 @@ object BList { 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)) - // case Impl(_,_,_) => Impl(self.offset, self.block.asInstanceOf[Array[B]], go(self.tailBList.asInstanceOf[Impl[A]])) } } - // for now only optimization is checking if either are empty + // for now, the only optimization is checking if either are empty (this, l2) match { case (_, Empty) => this case (_, _) => go(this) diff --git a/tests/src/test/scala/cats/collections/BListSuite.scala b/tests/src/test/scala/cats/collections/BListSuite.scala index cf6f732c..95047f5f 100644 --- a/tests/src/test/scala/cats/collections/BListSuite.scala +++ b/tests/src/test/scala/cats/collections/BListSuite.scala @@ -32,7 +32,6 @@ import org.scalacheck.Test import scala.math.pow class BListSuite extends DisciplineSuite { - // override def scalaCheckInitialSeed = "kueY5dMKVWrkQrfGXwqy_Wh4bK4-qnfvz_2D0-LrwPI=" override def scalaCheckTestParameters: Test.Parameters = DefaultScalaCheckPropertyCheckConfig.default @@ -244,6 +243,22 @@ class BListSuite extends DisciplineSuite { assertEquals(m.drop(-200), m) } + test("blocks don't matter for equality") { + // todo + val l1 = BList.empty[Int].prepend(3).prepend(2).prepend(1) + val m1 = BList.empty[Int].prepend(0).prepend(-1).prepend(-2) + + val l2 = BList.empty[Int].prepend(3).prepend(2) + val m2 = BList.empty[Int].prepend(1).prepend(0).prepend(-1).prepend(-2) + + val n1 = m1.concat(l1) + val n2 = m2.concat(l2) + println(n1.toStringInBlocks) + println(n2.toStringInBlocks) + assertEquals(n1, n2) + + } + private def testHomomorphism[A, B: Eq](as: BList[A])(fn: BList[A] => B, gn: List[A] => B): Unit = { val la = as.toList assert(Eq[B].eqv(fn(as), gn(la))) @@ -332,4 +347,15 @@ class BListSuite extends DisciplineSuite { property("fromListReverse == .reverse fromList")(forAll { (xs: List[Int]) => assertEquals(BList.fromListReverse(xs), BList.fromList(xs.reverse)) }) + + property("concat is associative")(forAll { (xs: BList[Int], ys: BList[Int], zs: BList[Int]) => + val l1 = xs.concat(ys).concat(zs) + val l2 = xs.concat(ys.concat(zs)) + assertEquals(l1, l2) + }) + property("== implies same toString for int lists")(forAll { (xs: BList[Int], ys: BList[Int]) => + if (xs.toString == ys.toString) { + assertEquals(xs, ys) + } + }) } From db06cc4b4a3a340a01ee31be87dad64bc0196289 Mon Sep 17 00:00:00 2001 From: Lola Denney Date: Thu, 18 Jun 2026 11:58:11 -0400 Subject: [PATCH 26/36] made another version of equals --- .../main/scala/cats/collections/BList.scala | 77 +++++++++++++++---- .../scala/cats/collections/BListSuite.scala | 3 +- 2 files changed, 65 insertions(+), 15 deletions(-) diff --git a/core/src/main/scala/cats/collections/BList.scala b/core/src/main/scala/cats/collections/BList.scala index efa9dbe2..9a83f76b 100644 --- a/core/src/main/scala/cats/collections/BList.scala +++ b/core/src/main/scala/cats/collections/BList.scala @@ -22,6 +22,7 @@ package cats.collections import scala.annotation.tailrec +import scala.annotation.unchecked.uncheckedVariance sealed abstract class BList[+A] { def uncons: Option[(A, BList[A])] @@ -118,8 +119,60 @@ object BList { Some((l.offset, l.block, l.tailBList)) } - private class Impl[A](val offset: Int, val block: Array[A], val tailBList: BList[A]) extends NonEmpty[A] { + private class Impl[+A](val offset: Int, val block: Array[A @uncheckedVariance], val tailBList: BList[A]) extends NonEmpty[A] { + + //equals version 2 + override def equals(other: Any): Boolean = { + // Helper recursive class-level function. start inclusive, end exclusive + // ary1 will be shorter or equal length to ary2 + def arrayEqualsPrefix(ary1: Array[_], ary2: Array[_], ary1start: Int, ary2start:Int): Boolean = { + //compare from ary1start until BlockSize with other ary from ary2start + var i = 0 + while ( i+ary1start < BlockSize) { + if (ary1(i+ary1start) != ary2(i+ary2start)) return false + i += 1 + } + true + } + + other match { + case that: Impl[_] => + // we know both lists are Impl here. + var list1:Impl[_] = this + var list2:Impl[_] = that + var list1idx = list1.offset + var list2idx = list2.offset + + while (!list1.isEmpty && !list2.isEmpty){ + if (list1idx == list2idx){ + if (!arrayEqualsPrefix(list1.block, list2.block, list1idx, list2idx)) return false // TODO if this is false return false + if (list1.tailBList.isEmpty || list2.tailBList.isEmpty) { + return list1.tailBList.isEmpty && list2.tailBList.isEmpty + } + list1 = list1.tailBList.asInstanceOf[Impl[_]] + list2 = list2.tailBList.asInstanceOf[Impl[_]] + list1idx = list1.offset + list2idx = list2.offset + } else if (list1idx > list2idx){ + if ((list1.tailBList.isEmpty) || !arrayEqualsPrefix(list1.block, list2.block, list1idx, list2idx) ) return false + list2idx = list2idx + (BlockSize - list1idx) + list1 = list1.tailBList.asInstanceOf[Impl[_]] + list1idx = list1.offset + } else { // (list1idx < list2idx) + if ((list2.tailBList.isEmpty) || ! arrayEqualsPrefix(list2.block, list1.block, list2idx, list1idx)) return false + list1idx = list1idx + (BlockSize - list2idx) + list2 = list2.tailBList.asInstanceOf[Impl[_]] + list2idx = list2.offset + } + + } + true + case _ => false + } + } + /* + //equals version 1 - i think i will try to test the performance of the two versions against each other override def equals(other: Any): Boolean = { // helper to get next element of BList def next[B](node: Impl[B], curoffset: Int): Option[(B, Impl[B], Int)] = { @@ -127,7 +180,7 @@ object BList { Some((node.block(curoffset + 1), node, curoffset + 1)) } else { node.tailBList match { - case Empty => None + case _: Empty.type => None case tail: Impl[B] @unchecked => // there will be at least one element in every block Some((tail.block(tail.offset), tail, tail.offset)) } @@ -139,19 +192,15 @@ object BList { // 3-tuple has structure: element, node where it is found, offset at which it was found @tailrec def loop[B](list1: (A, Impl[A], Int), list2: (B, Impl[B], Int)): Boolean = { - if (list1._1 != list2._1) { - false - } else { - - (next(list1._2, list1._3), next(list2._2, list2._3)) match { - case (None, None) => true - case (None, _) | (_, None) => false - case (Some(list1_tail), Some(list2_tail)) => - loop(list1_tail, list2_tail) - } + if (list1._1 != list2._1) return false + (next(list1._2, list1._3), next(list2._2, list2._3)) match { + case (None, None) => true + case (None, _) | (_, None) => false + case (Some(list1_tail), Some(list2_tail)) => + loop(list1_tail, list2_tail) } - + } loop( @@ -162,7 +211,7 @@ object BList { case _ => false } } - + */ override def hashCode(): Int = { // uses only the valid elements in the first block var res = 31 diff --git a/tests/src/test/scala/cats/collections/BListSuite.scala b/tests/src/test/scala/cats/collections/BListSuite.scala index 95047f5f..0dba7206 100644 --- a/tests/src/test/scala/cats/collections/BListSuite.scala +++ b/tests/src/test/scala/cats/collections/BListSuite.scala @@ -45,7 +45,8 @@ class BListSuite extends DisciplineSuite { val n1 = m1.concat(l1) val n2 = m2.concat(l2) - + println(n1.toStringInBlocks) + println(n2.toStringInBlocks) assertEquals(n1, n2) } From ffde5b5d7036f4ad012fcfa96682d11c89a42af3 Mon Sep 17 00:00:00 2001 From: Lola Denney Date: Thu, 18 Jun 2026 11:59:08 -0400 Subject: [PATCH 27/36] forgot to run formatter --- .../main/scala/cats/collections/BList.scala | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/core/src/main/scala/cats/collections/BList.scala b/core/src/main/scala/cats/collections/BList.scala index 9a83f76b..09e4567d 100644 --- a/core/src/main/scala/cats/collections/BList.scala +++ b/core/src/main/scala/cats/collections/BList.scala @@ -119,17 +119,18 @@ object BList { 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] { - - //equals version 2 + private class Impl[+A](val offset: Int, val block: Array[A @uncheckedVariance], val tailBList: BList[A]) + extends NonEmpty[A] { + + // equals version 2 override def equals(other: Any): Boolean = { // Helper recursive class-level function. start inclusive, end exclusive // ary1 will be shorter or equal length to ary2 - def arrayEqualsPrefix(ary1: Array[_], ary2: Array[_], ary1start: Int, ary2start:Int): Boolean = { - //compare from ary1start until BlockSize with other ary from ary2start + def arrayEqualsPrefix(ary1: Array[_], ary2: Array[_], ary1start: Int, ary2start: Int): Boolean = { + // compare from ary1start until BlockSize with other ary from ary2start var i = 0 - while ( i+ary1start < BlockSize) { - if (ary1(i+ary1start) != ary2(i+ary2start)) return false + while (i + ary1start < BlockSize) { + if (ary1(i + ary1start) != ary2(i + ary2start)) return false i += 1 } true @@ -137,15 +138,16 @@ object BList { other match { case that: Impl[_] => - // we know both lists are Impl here. - var list1:Impl[_] = this - var list2:Impl[_] = that + // we know both lists are Impl here. + var list1: Impl[_] = this + var list2: Impl[_] = that var list1idx = list1.offset var list2idx = list2.offset - while (!list1.isEmpty && !list2.isEmpty){ - if (list1idx == list2idx){ - if (!arrayEqualsPrefix(list1.block, list2.block, list1idx, list2idx)) return false // TODO if this is false return false + while (!list1.isEmpty && !list2.isEmpty) { + if (list1idx == list2idx) { + if (!arrayEqualsPrefix(list1.block, list2.block, list1idx, list2idx)) + return false // TODO if this is false return false if (list1.tailBList.isEmpty || list2.tailBList.isEmpty) { return list1.tailBList.isEmpty && list2.tailBList.isEmpty } @@ -153,18 +155,20 @@ object BList { list2 = list2.tailBList.asInstanceOf[Impl[_]] list1idx = list1.offset list2idx = list2.offset - } else if (list1idx > list2idx){ - if ((list1.tailBList.isEmpty) || !arrayEqualsPrefix(list1.block, list2.block, list1idx, list2idx) ) return false + } else if (list1idx > list2idx) { + if ((list1.tailBList.isEmpty) || !arrayEqualsPrefix(list1.block, list2.block, list1idx, list2idx)) + return false list2idx = list2idx + (BlockSize - list1idx) list1 = list1.tailBList.asInstanceOf[Impl[_]] list1idx = list1.offset } else { // (list1idx < list2idx) - if ((list2.tailBList.isEmpty) || ! arrayEqualsPrefix(list2.block, list1.block, list2idx, list1idx)) return false + if ((list2.tailBList.isEmpty) || !arrayEqualsPrefix(list2.block, list1.block, list2idx, list1idx)) + return false list1idx = list1idx + (BlockSize - list2idx) list2 = list2.tailBList.asInstanceOf[Impl[_]] list2idx = list2.offset } - + } true @@ -211,7 +215,7 @@ object BList { case _ => false } } - */ + */ override def hashCode(): Int = { // uses only the valid elements in the first block var res = 31 From 0bf49bdc5bf9d4178b8db9907ad4cf401a685203 Mon Sep 17 00:00:00 2001 From: Lola Denney Date: Fri, 19 Jun 2026 10:42:08 -0400 Subject: [PATCH 28/36] changed hashcode to match other scala list-like datastructures --- .../main/scala/cats/collections/BList.scala | 101 ++++++++++++++++-- .../scala/cats/collections/BListSuite.scala | 4 - 2 files changed, 92 insertions(+), 13 deletions(-) diff --git a/core/src/main/scala/cats/collections/BList.scala b/core/src/main/scala/cats/collections/BList.scala index 09e4567d..84c8f6a0 100644 --- a/core/src/main/scala/cats/collections/BList.scala +++ b/core/src/main/scala/cats/collections/BList.scala @@ -23,6 +23,7 @@ package cats.collections import scala.annotation.tailrec import scala.annotation.unchecked.uncheckedVariance +import scala.util.hashing.MurmurHash3.seqSeed sealed abstract class BList[+A] { def uncons: Option[(A, BList[A])] @@ -40,13 +41,12 @@ sealed abstract class BList[+A] { def toList: List[A] def isEmpty: Boolean - // for development and testing - private[collections] def toStringInBlocks: String - 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(") @@ -65,6 +65,8 @@ sealed abstract class BList[+A] { strb.toString } + def equals(other: Any): Boolean + def hashCode(): Int } object BList { @@ -92,6 +94,13 @@ object BList { def isEmpty: Boolean = true private[collections] def toStringInBlocks: String = "Empty" + override def equals(other: Any): Boolean = other match { + case _: Empty.type => true + case _ => false + } + + override def hashCode(): Int = finalizeHash(seqSeed, 0) + } sealed abstract class NonEmpty[+A] extends BList[A] { def head: A @@ -216,13 +225,53 @@ object BList { } } */ + + /* + * HashCode computation for BList is adapted from https://github.com/scala/scala/blob/e48c7888a0ae08b8d273f03bfc62452fc2ed886d/src/library/scala/util/hashing/MurmurHash3.scala + * such that BList hashcodes are consistent with other list-like datatypes in the Scala standard library. + */ override def hashCode(): Int = { - // uses only the valid elements in the first block - var res = 31 - for (i <- this.offset until BlockSize) { - res = res * 31 + this.block(i).hashCode() + var n = 0 + var h = seqSeed + var rangeState = 0 // 0 = no data, 1 = first elem read, 2 = has valid diff, 3 = invalid + var rangeDiff = 0 + var prev = 0 + var initial = 0 + var curoffset = this.offset + var elems: Impl[_] = this + + var elemsIsNotEmpty = true + while (elemsIsNotEmpty) { // change this check to be later i think + val head = elems.block(curoffset) + val hash = head.## + h = mix(h, hash) + rangeState match { + case 0 => + initial = hash + rangeState = 1 + case 1 => + rangeDiff = hash - prev + rangeState = 2 + case 2 => + if (rangeDiff != hash - prev) rangeState = 3 + case _ => + } + prev = hash + n += 1 + curoffset += 1 + + // logic for proceeding to next block + if (curoffset >= BlockSize) { + elems.tailBList match { + case Empty => elemsIsNotEmpty = false // break loop + case nextBlock: Impl[_] @unchecked => + elems = nextBlock + curoffset = nextBlock.offset + } + } } - res + if (rangeState == 2) rangeHash(initial, rangeDiff, prev, seqSeed) + else finalizeHash(h, n) } def uncons: Some[(A, BList[A])] = { @@ -437,4 +486,38 @@ object BList { } def empty[A]: BList[A] = Empty + + /* + * The following five methods are copied from https://github.com/scala/scala/blob/e48c7888a0ae08b8d273f03bfc62452fc2ed886d/src/library/scala/util/hashing/MurmurHash3.scala + * They are helper functions for computing a hashcode for BList that is consistent with that of other + * list-like datatypes in the Scala standard library (List, Vector, Range etc.) + */ + final private def finalizeHash(hash: Int, length: Int): Int = avalanche(hash ^ length) + final private def avalanche(hash: Int): Int = { + var h = hash + h ^= h >>> 16 + h *= 0x85ebca6b + h ^= h >>> 13 + h *= 0xc2b2ae35 + h ^= h >>> 16 + h + } + final private def mixLast(hash: Int, data: Int): Int = { + var k = data + + k *= 0xcc9e2d51 + k = java.lang.Integer.rotateLeft(k, 15) + k *= 0x1b873593 + + hash ^ k + } + final private def mix(hash: Int, data: Int): Int = { + var h = mixLast(hash, data) + h = java.lang.Integer.rotateLeft(h, 13) + h * 5 + 0xe6546b64 + } + // this one isn't available in scala 2.12, so it is included in this file + final private def rangeHash(start: Int, step: Int, last: Int, seed: Int): Int = + avalanche(mix(mix(mix(seed, start), step), last)) + } diff --git a/tests/src/test/scala/cats/collections/BListSuite.scala b/tests/src/test/scala/cats/collections/BListSuite.scala index 0dba7206..3affdb4f 100644 --- a/tests/src/test/scala/cats/collections/BListSuite.scala +++ b/tests/src/test/scala/cats/collections/BListSuite.scala @@ -45,8 +45,6 @@ class BListSuite extends DisciplineSuite { val n1 = m1.concat(l1) val n2 = m2.concat(l2) - println(n1.toStringInBlocks) - println(n2.toStringInBlocks) assertEquals(n1, n2) } @@ -254,8 +252,6 @@ class BListSuite extends DisciplineSuite { val n1 = m1.concat(l1) val n2 = m2.concat(l2) - println(n1.toStringInBlocks) - println(n2.toStringInBlocks) assertEquals(n1, n2) } From f18b0e5cad82247f1bd1422d76ea86ebeea98afd Mon Sep 17 00:00:00 2001 From: Lola Denney Date: Tue, 23 Jun 2026 13:35:48 -0400 Subject: [PATCH 29/36] removed usedless cogen param --- .../cats/collections/arbitrary/ArbitraryBList.scala | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/scalacheck/src/main/scala/cats/collections/arbitrary/ArbitraryBList.scala b/scalacheck/src/main/scala/cats/collections/arbitrary/ArbitraryBList.scala index 81ca3e29..e8d82a5b 100644 --- a/scalacheck/src/main/scala/cats/collections/arbitrary/ArbitraryBList.scala +++ b/scalacheck/src/main/scala/cats/collections/arbitrary/ArbitraryBList.scala @@ -24,11 +24,10 @@ package cats.collections.arbitrary import cats.collections.BList import org.scalacheck.Arbitrary import org.scalacheck.Arbitrary.arbitrary -import org.scalacheck.Cogen import org.scalacheck.Gen trait ArbitraryBList { - def bListGen[A](implicit arb: Arbitrary[A], cogen: Cogen[A]): Gen[BList[A]] = { + def bListGen[A](implicit arb: Arbitrary[A]): Gen[BList[A]] = { Gen.sized { case 0 => Gen.const(BList.empty) case n => @@ -46,15 +45,15 @@ trait ArbitraryBList { ys <- Gen.resize(n / 4, bListGen[A]) } yield xs.concat(ys), // map - Gen.resize(n / 2, bListGen[A]).flatMap { l => - arbitrary[A => A].map(fn => l.map(fn)) + Gen.resize(n / 2, bListGen[A]).map { l => + l.map(identity) } ) } } - implicit def arbitraryBList[A](implicit arb: Arbitrary[A], cogen: Cogen[A]): Arbitrary[BList[A]] = - Arbitrary(bListGen(arb, cogen)) + implicit def arbitraryBList[A](implicit arb: Arbitrary[A]): Arbitrary[BList[A]] = + Arbitrary(bListGen(arb)) } object ArbitraryBList extends ArbitraryBList From cb7094500856d3d6d6d27defcb4d6ce0edfa9715 Mon Sep 17 00:00:00 2001 From: Lola Denney Date: Wed, 24 Jun 2026 13:36:57 -0400 Subject: [PATCH 30/36] fixing issues with toList caused by mutable builder --- core/src/main/scala/cats/collections/BList.scala | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/src/main/scala/cats/collections/BList.scala b/core/src/main/scala/cats/collections/BList.scala index 84c8f6a0..01b32508 100644 --- a/core/src/main/scala/cats/collections/BList.scala +++ b/core/src/main/scala/cats/collections/BList.scala @@ -430,19 +430,19 @@ object BList { } override def toList: List[A] = { - val builder = List.newBuilder[A] @tailrec - def loop(l: BList[A]): List[A] = + def loop(l: BList[A], acc: List[A]): List[A] = l match { - case Empty => builder.result() + case Empty => acc case impl: Impl[A] @unchecked => + var newstuff: List[A] = List() // append valid things in the block to acc for (i <- impl.offset until BlockSize) { - builder += impl.block(i) + newstuff = newstuff :+ impl.block(i) } - loop(impl.tailBList) + loop(impl.tailBList, acc ++ newstuff) } - loop(this) + loop(this, List[A]()) } def isEmpty: Boolean = false From ce7635207cd06738f1641d232d763dab6f8eb35b Mon Sep 17 00:00:00 2001 From: Lola Denney Date: Thu, 25 Jun 2026 10:20:15 -0400 Subject: [PATCH 31/36] Revert "removed usedless cogen param" This reverts commit f18b0e5cad82247f1bd1422d76ea86ebeea98afd. this reverses changes to toList --- .../cats/collections/arbitrary/ArbitraryBList.scala | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/scalacheck/src/main/scala/cats/collections/arbitrary/ArbitraryBList.scala b/scalacheck/src/main/scala/cats/collections/arbitrary/ArbitraryBList.scala index e8d82a5b..81ca3e29 100644 --- a/scalacheck/src/main/scala/cats/collections/arbitrary/ArbitraryBList.scala +++ b/scalacheck/src/main/scala/cats/collections/arbitrary/ArbitraryBList.scala @@ -24,10 +24,11 @@ package cats.collections.arbitrary import cats.collections.BList import org.scalacheck.Arbitrary import org.scalacheck.Arbitrary.arbitrary +import org.scalacheck.Cogen import org.scalacheck.Gen trait ArbitraryBList { - def bListGen[A](implicit arb: Arbitrary[A]): Gen[BList[A]] = { + def bListGen[A](implicit arb: Arbitrary[A], cogen: Cogen[A]): Gen[BList[A]] = { Gen.sized { case 0 => Gen.const(BList.empty) case n => @@ -45,15 +46,15 @@ trait ArbitraryBList { ys <- Gen.resize(n / 4, bListGen[A]) } yield xs.concat(ys), // map - Gen.resize(n / 2, bListGen[A]).map { l => - l.map(identity) + Gen.resize(n / 2, bListGen[A]).flatMap { l => + arbitrary[A => A].map(fn => l.map(fn)) } ) } } - implicit def arbitraryBList[A](implicit arb: Arbitrary[A]): Arbitrary[BList[A]] = - Arbitrary(bListGen(arb)) + implicit def arbitraryBList[A](implicit arb: Arbitrary[A], cogen: Cogen[A]): Arbitrary[BList[A]] = + Arbitrary(bListGen(arb, cogen)) } object ArbitraryBList extends ArbitraryBList From 0c35b05b1738e0dd10f46965fe4af3e1d2d41e12 Mon Sep 17 00:00:00 2001 From: Lola Denney Date: Tue, 23 Jun 2026 13:35:48 -0400 Subject: [PATCH 32/36] removed usedless cogen param --- .../cats/collections/arbitrary/ArbitraryBList.scala | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/scalacheck/src/main/scala/cats/collections/arbitrary/ArbitraryBList.scala b/scalacheck/src/main/scala/cats/collections/arbitrary/ArbitraryBList.scala index 81ca3e29..e8d82a5b 100644 --- a/scalacheck/src/main/scala/cats/collections/arbitrary/ArbitraryBList.scala +++ b/scalacheck/src/main/scala/cats/collections/arbitrary/ArbitraryBList.scala @@ -24,11 +24,10 @@ package cats.collections.arbitrary import cats.collections.BList import org.scalacheck.Arbitrary import org.scalacheck.Arbitrary.arbitrary -import org.scalacheck.Cogen import org.scalacheck.Gen trait ArbitraryBList { - def bListGen[A](implicit arb: Arbitrary[A], cogen: Cogen[A]): Gen[BList[A]] = { + def bListGen[A](implicit arb: Arbitrary[A]): Gen[BList[A]] = { Gen.sized { case 0 => Gen.const(BList.empty) case n => @@ -46,15 +45,15 @@ trait ArbitraryBList { ys <- Gen.resize(n / 4, bListGen[A]) } yield xs.concat(ys), // map - Gen.resize(n / 2, bListGen[A]).flatMap { l => - arbitrary[A => A].map(fn => l.map(fn)) + Gen.resize(n / 2, bListGen[A]).map { l => + l.map(identity) } ) } } - implicit def arbitraryBList[A](implicit arb: Arbitrary[A], cogen: Cogen[A]): Arbitrary[BList[A]] = - Arbitrary(bListGen(arb, cogen)) + implicit def arbitraryBList[A](implicit arb: Arbitrary[A]): Arbitrary[BList[A]] = + Arbitrary(bListGen(arb)) } object ArbitraryBList extends ArbitraryBList From 8659fea9fa1ef0b7a19e342f7691b8eaece9e87b Mon Sep 17 00:00:00 2001 From: Lola Denney Date: Thu, 25 Jun 2026 10:25:11 -0400 Subject: [PATCH 33/36] Revert "fixing issues with toList caused by mutable builder" This reverts commit cb7094500856d3d6d6d27defcb4d6ce0edfa9715. the last revert I did on the wrong commit. this is the correct one, to undo changes to toList --- core/src/main/scala/cats/collections/BList.scala | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/src/main/scala/cats/collections/BList.scala b/core/src/main/scala/cats/collections/BList.scala index 01b32508..84c8f6a0 100644 --- a/core/src/main/scala/cats/collections/BList.scala +++ b/core/src/main/scala/cats/collections/BList.scala @@ -430,19 +430,19 @@ object BList { } override def toList: List[A] = { + val builder = List.newBuilder[A] @tailrec - def loop(l: BList[A], acc: List[A]): List[A] = + def loop(l: BList[A]): List[A] = l match { - case Empty => acc + case Empty => builder.result() case impl: Impl[A] @unchecked => - var newstuff: List[A] = List() // append valid things in the block to acc for (i <- impl.offset until BlockSize) { - newstuff = newstuff :+ impl.block(i) + builder += impl.block(i) } - loop(impl.tailBList, acc ++ newstuff) + loop(impl.tailBList) } - loop(this, List[A]()) + loop(this) } def isEmpty: Boolean = false From 7325bf8efef8fae46f71a18f93a43f0471191b62 Mon Sep 17 00:00:00 2001 From: Lola Denney Date: Thu, 25 Jun 2026 15:50:33 -0400 Subject: [PATCH 34/36] iterator, and using it for hashcode and equals --- .../main/scala/cats/collections/BList.scala | 207 ++++-------------- 1 file changed, 38 insertions(+), 169 deletions(-) diff --git a/core/src/main/scala/cats/collections/BList.scala b/core/src/main/scala/cats/collections/BList.scala index 84c8f6a0..b9eafc97 100644 --- a/core/src/main/scala/cats/collections/BList.scala +++ b/core/src/main/scala/cats/collections/BList.scala @@ -23,7 +23,7 @@ package cats.collections import scala.annotation.tailrec import scala.annotation.unchecked.uncheckedVariance -import scala.util.hashing.MurmurHash3.seqSeed +import scala.util.hashing.MurmurHash3 sealed abstract class BList[+A] { def uncons: Option[(A, BList[A])] @@ -40,6 +40,7 @@ sealed abstract class BList[+A] { def concat[B >: A](l2: BList[B]): BList[B] def toList: List[A] def isEmpty: Boolean + def toIterator: Iterator[A] final def ::[B >: A](a: B): BList[B] = prepend(a) final def ++[B >: A](l2: BList[B]): BList[B] = concat(l2) @@ -92,6 +93,7 @@ object BList { def concat[B](l2: BList[B]): BList[B] = l2 override def toList: List[Nothing] = Nil def isEmpty: Boolean = true + def toIterator: Iterator[Nothing] = new BListIterator(this) private[collections] def toStringInBlocks: String = "Empty" override def equals(other: Any): Boolean = other match { @@ -99,7 +101,7 @@ object BList { case _ => false } - override def hashCode(): Int = finalizeHash(seqSeed, 0) + override def hashCode(): Int = Empty.toIterator.## } sealed abstract class NonEmpty[+A] extends BList[A] { @@ -131,148 +133,22 @@ object BList { private class Impl[+A](val offset: Int, val block: Array[A @uncheckedVariance], val tailBList: BList[A]) extends NonEmpty[A] { - // equals version 2 - override def equals(other: Any): Boolean = { - // Helper recursive class-level function. start inclusive, end exclusive - // ary1 will be shorter or equal length to ary2 - def arrayEqualsPrefix(ary1: Array[_], ary2: Array[_], ary1start: Int, ary2start: Int): Boolean = { - // compare from ary1start until BlockSize with other ary from ary2start - var i = 0 - while (i + ary1start < BlockSize) { - if (ary1(i + ary1start) != ary2(i + ary2start)) return false - i += 1 - } - true - } - + override def equals(other: Any): Boolean = other match { - case that: Impl[_] => - // we know both lists are Impl here. - var list1: Impl[_] = this - var list2: Impl[_] = that - var list1idx = list1.offset - var list2idx = list2.offset - - while (!list1.isEmpty && !list2.isEmpty) { - if (list1idx == list2idx) { - if (!arrayEqualsPrefix(list1.block, list2.block, list1idx, list2idx)) - return false // TODO if this is false return false - if (list1.tailBList.isEmpty || list2.tailBList.isEmpty) { - return list1.tailBList.isEmpty && list2.tailBList.isEmpty - } - list1 = list1.tailBList.asInstanceOf[Impl[_]] - list2 = list2.tailBList.asInstanceOf[Impl[_]] - list1idx = list1.offset - list2idx = list2.offset - } else if (list1idx > list2idx) { - if ((list1.tailBList.isEmpty) || !arrayEqualsPrefix(list1.block, list2.block, list1idx, list2idx)) - return false - list2idx = list2idx + (BlockSize - list1idx) - list1 = list1.tailBList.asInstanceOf[Impl[_]] - list1idx = list1.offset - } else { // (list1idx < list2idx) - if ((list2.tailBList.isEmpty) || !arrayEqualsPrefix(list2.block, list1.block, list2idx, list1idx)) - return false - list1idx = list1idx + (BlockSize - list2idx) - list2 = list2.tailBList.asInstanceOf[Impl[_]] - list2idx = list2.offset - } - - } - true - - case _ => false - } - } - /* - //equals version 1 - i think i will try to test the performance of the two versions against each other - override def equals(other: Any): Boolean = { - // helper to get next element of BList - def next[B](node: Impl[B], curoffset: Int): Option[(B, Impl[B], Int)] = { - if (curoffset < BlockSize - 1) { - Some((node.block(curoffset + 1), node, curoffset + 1)) - } else { - node.tailBList match { - case _: Empty.type => None - case tail: Impl[B] @unchecked => // there will be at least one element in every block - Some((tail.block(tail.offset), tail, tail.offset)) - } - } - } - - other match { - case that: Impl[_] => - // 3-tuple has structure: element, node where it is found, offset at which it was found - @tailrec - def loop[B](list1: (A, Impl[A], Int), list2: (B, Impl[B], Int)): Boolean = { - if (list1._1 != list2._1) return false - - (next(list1._2, list1._3), next(list2._2, list2._3)) match { - case (None, None) => true - case (None, _) | (_, None) => false - case (Some(list1_tail), Some(list2_tail)) => - loop(list1_tail, list2_tail) + case o: Impl[_] => + val iterX = this.toIterator + val iterY = o.toIterator + while (iterX.hasNext && iterY.hasNext) { + if (iterX.next() != iterY.next()) { // not sure if i could be comparing with != here... + return false } - } - - loop( - (this.block(this.offset), this, this.offset), // list1 - (that.block(that.offset), that, that.offset) // list2 - ) - + iterX.hasNext == iterY.hasNext case _ => false } - } - */ - - /* - * HashCode computation for BList is adapted from https://github.com/scala/scala/blob/e48c7888a0ae08b8d273f03bfc62452fc2ed886d/src/library/scala/util/hashing/MurmurHash3.scala - * such that BList hashcodes are consistent with other list-like datatypes in the Scala standard library. - */ - override def hashCode(): Int = { - var n = 0 - var h = seqSeed - var rangeState = 0 // 0 = no data, 1 = first elem read, 2 = has valid diff, 3 = invalid - var rangeDiff = 0 - var prev = 0 - var initial = 0 - var curoffset = this.offset - var elems: Impl[_] = this - - var elemsIsNotEmpty = true - while (elemsIsNotEmpty) { // change this check to be later i think - val head = elems.block(curoffset) - val hash = head.## - h = mix(h, hash) - rangeState match { - case 0 => - initial = hash - rangeState = 1 - case 1 => - rangeDiff = hash - prev - rangeState = 2 - case 2 => - if (rangeDiff != hash - prev) rangeState = 3 - case _ => - } - prev = hash - n += 1 - curoffset += 1 - // logic for proceeding to next block - if (curoffset >= BlockSize) { - elems.tailBList match { - case Empty => elemsIsNotEmpty = false // break loop - case nextBlock: Impl[_] @unchecked => - elems = nextBlock - curoffset = nextBlock.offset - } - } - } - if (rangeState == 2) rangeHash(initial, rangeDiff, prev, seqSeed) - else finalizeHash(h, n) - } + override def hashCode: Int = + MurmurHash3.orderedHash(this.toIterator) def uncons: Some[(A, BList[A])] = { Some((block(offset), this.tail)) @@ -444,7 +320,9 @@ object BList { } loop(this) } + def isEmpty: Boolean = false + def toIterator: Iterator[A] = new BListIterator(this) private[collections] def toStringInBlocks: String = { val strb = new java.lang.StringBuilder @@ -487,37 +365,28 @@ object BList { def empty[A]: BList[A] = Empty - /* - * The following five methods are copied from https://github.com/scala/scala/blob/e48c7888a0ae08b8d273f03bfc62452fc2ed886d/src/library/scala/util/hashing/MurmurHash3.scala - * They are helper functions for computing a hashcode for BList that is consistent with that of other - * list-like datatypes in the Scala standard library (List, Vector, Range etc.) - */ - final private def finalizeHash(hash: Int, length: Int): Int = avalanche(hash ^ length) - final private def avalanche(hash: Int): Int = { - var h = hash - h ^= h >>> 16 - h *= 0x85ebca6b - h ^= h >>> 13 - h *= 0xc2b2ae35 - h ^= h >>> 16 - h - } - final private def mixLast(hash: Int, data: Int): Int = { - var k = data - - k *= 0xcc9e2d51 - k = java.lang.Integer.rotateLeft(k, 15) - k *= 0x1b873593 + final class BListIterator[A](from: BList[A]) extends Iterator[A] { + private var curNode: BList[A] = from + private var curOffset: Int = if (!from.isEmpty) curNode.asInstanceOf[Impl[A]].offset else BlockSize + + def hasNext: Boolean = !curNode.isEmpty + def next(): A = + curNode match { + case Empty => throw new NoSuchElementException + case impl: Impl[A] => { + val next = impl.block(curOffset) + curOffset += 1 + if (curOffset >= BlockSize) { + // advance to next block + curNode = impl.tailBList + curOffset = curNode match { + case Empty => BlockSize + case impl_prime: Impl[A] => impl_prime.offset + } + } + next + } + } - hash ^ k } - final private def mix(hash: Int, data: Int): Int = { - var h = mixLast(hash, data) - h = java.lang.Integer.rotateLeft(h, 13) - h * 5 + 0xe6546b64 - } - // this one isn't available in scala 2.12, so it is included in this file - final private def rangeHash(start: Int, step: Int, last: Int, seed: Int): Int = - avalanche(mix(mix(mix(seed, start), step), last)) - } From 62bee70b1062f36cfda04d882d117a2bb6938d4b Mon Sep 17 00:00:00 2001 From: L Denney <156242030+loladenney@users.noreply.github.com> Date: Mon, 29 Jun 2026 14:48:00 -0400 Subject: [PATCH 35/36] Update core/src/main/scala/cats/collections/BList.scala Co-authored-by: Sergey Torgashov --- core/src/main/scala/cats/collections/BList.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/scala/cats/collections/BList.scala b/core/src/main/scala/cats/collections/BList.scala index b9eafc97..e10ff290 100644 --- a/core/src/main/scala/cats/collections/BList.scala +++ b/core/src/main/scala/cats/collections/BList.scala @@ -93,7 +93,7 @@ object BList { def concat[B](l2: BList[B]): BList[B] = l2 override def toList: List[Nothing] = Nil def isEmpty: Boolean = true - def toIterator: Iterator[Nothing] = new BListIterator(this) + def toIterator: Iterator[Nothing] = Iterator.empty private[collections] def toStringInBlocks: String = "Empty" override def equals(other: Any): Boolean = other match { From 12d460bfde9e3e759d3926a2dff3dca97335a7a0 Mon Sep 17 00:00:00 2001 From: Lola Denney Date: Mon, 29 Jun 2026 15:30:53 -0400 Subject: [PATCH 36/36] changes to iterator --- .../main/scala/cats/collections/BList.scala | 65 ++++++++----------- 1 file changed, 28 insertions(+), 37 deletions(-) diff --git a/core/src/main/scala/cats/collections/BList.scala b/core/src/main/scala/cats/collections/BList.scala index e10ff290..ab135569 100644 --- a/core/src/main/scala/cats/collections/BList.scala +++ b/core/src/main/scala/cats/collections/BList.scala @@ -40,7 +40,7 @@ sealed abstract class BList[+A] { def concat[B >: A](l2: BList[B]): BList[B] def toList: List[A] def isEmpty: Boolean - def toIterator: Iterator[A] + 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) @@ -93,7 +93,7 @@ object BList { def concat[B](l2: BList[B]): BList[B] = l2 override def toList: List[Nothing] = Nil def isEmpty: Boolean = true - def toIterator: Iterator[Nothing] = Iterator.empty + def iterator: Iterator[Nothing] = Iterator.empty private[collections] def toStringInBlocks: String = "Empty" override def equals(other: Any): Boolean = other match { @@ -101,7 +101,7 @@ object BList { case _ => false } - override def hashCode(): Int = Empty.toIterator.## + override def hashCode(): Int = Empty.iterator.## } sealed abstract class NonEmpty[+A] extends BList[A] { @@ -136,19 +136,12 @@ object BList { override def equals(other: Any): Boolean = other match { case o: Impl[_] => - val iterX = this.toIterator - val iterY = o.toIterator - while (iterX.hasNext && iterY.hasNext) { - if (iterX.next() != iterY.next()) { // not sure if i could be comparing with != here... - return false - } - } - iterX.hasNext == iterY.hasNext + this.iterator.sameElements(o.iterator.asInstanceOf[Iterator[A]]) case _ => false } override def hashCode: Int = - MurmurHash3.orderedHash(this.toIterator) + MurmurHash3.orderedHash(this.iterator) def uncons: Some[(A, BList[A])] = { Some((block(offset), this.tail)) @@ -322,7 +315,7 @@ object BList { } def isEmpty: Boolean = false - def toIterator: Iterator[A] = new BListIterator(this) + def iterator: Iterator[A] = new BListIterator(this) private[collections] def toStringInBlocks: String = { val strb = new java.lang.StringBuilder @@ -347,6 +340,28 @@ object BList { 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] = @@ -365,28 +380,4 @@ object BList { def empty[A]: BList[A] = Empty - final class BListIterator[A](from: BList[A]) extends Iterator[A] { - private var curNode: BList[A] = from - private var curOffset: Int = if (!from.isEmpty) curNode.asInstanceOf[Impl[A]].offset else BlockSize - - def hasNext: Boolean = !curNode.isEmpty - def next(): A = - curNode match { - case Empty => throw new NoSuchElementException - case impl: Impl[A] => { - val next = impl.block(curOffset) - curOffset += 1 - if (curOffset >= BlockSize) { - // advance to next block - curNode = impl.tailBList - curOffset = curNode match { - case Empty => BlockSize - case impl_prime: Impl[A] => impl_prime.offset - } - } - next - } - } - - } }