Skip to content

Commit

Permalink
[data] add ChunkBuilder to create StrictOptimizedSeqFactory[Chunk] (#…
Browse files Browse the repository at this point in the history
…889)

Co-authored-by: Flavio Brasil <fwbrasil@users.noreply.github.com>
  • Loading branch information
hearnadam and fwbrasil authored Dec 3, 2024
1 parent a31db06 commit 5d5f0c7
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 48 deletions.
70 changes: 23 additions & 47 deletions kyo-data/shared/src/main/scala/kyo/Chunk.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Chunk.Indexed
import java.util.Arrays
import scala.annotation.tailrec
import scala.annotation.targetName
import scala.collection.StrictOptimizedSeqFactory
import scala.reflect.ClassTag

/** An immutable, efficient sequence of elements.
Expand Down Expand Up @@ -354,9 +355,7 @@ sealed abstract class Chunk[A] extends Seq[A] derives CanEqual:
c.toArray

end Chunk

object Chunk:

object Chunk extends StrictOptimizedSeqFactory[Chunk]:
import internal.*

/** An indexed version of Chunk that provides O(1) access to elements.
Expand Down Expand Up @@ -428,20 +427,8 @@ object Chunk:
* @return
* an empty Chunk of type A
*/
def empty[A]: Chunk[A] =
cachedEmpty.asInstanceOf[Chunk[A]]

/** Creates a Chunk from a variable number of elements.
*
* @tparam A
* the type of elements in the Chunk
* @param values
* the elements to include in the Chunk
* @return
* a new Chunk containing the provided values
*/
def apply[A](values: A*): Chunk[A] =
from(values)
def empty[A]: Chunk[A] = indexedEmpty[A]
private[kyo] inline def indexedEmpty[A]: Indexed[A] = cachedEmpty.asInstanceOf[Indexed[A]]

/** Creates a Chunk from an Array of elements.
*
Expand All @@ -453,55 +440,44 @@ object Chunk:
* a new Chunk.Indexed containing the elements from the Array
*/
def from[A](values: Array[A]): Chunk.Indexed[A] =
if values.isEmpty then cachedEmpty.asInstanceOf[Chunk.Indexed[A]]
if values.isEmpty then indexedEmpty[A]
else
Compact(Array.copyAs(values, values.length)(using ClassTag.AnyRef).asInstanceOf[Array[A]])
end from

private[kyo] def fromNoCopy[A](values: Array[A]): Chunk.Indexed[A] =
if values.isEmpty then cachedEmpty.asInstanceOf[Chunk.Indexed[A]]
if values.isEmpty then indexedEmpty[A]
else
Compact(values)

/** Creates a Chunk from a Seq of elements.
/** Creates a Chunk from an `IterableOnce`.
*
* NOTE: This method will **mutate** the source `IterableOnce` if it is a mutable collection.
*
* @tparam A
* the type of elements in the Seq
* @param values
* the Seq to create the Chunk from
* the type of elements in the IterableOnce
* @param source
* the IterableOnce to create the Chunk from
* @return
* a new Chunk.Indexed containing the elements from the Seq
* a new Chunk.Indexed containing the elements from the IterableOnce
*/
def from[A](values: Seq[A]): Chunk.Indexed[A] =
if values.isEmpty then cachedEmpty.asInstanceOf[Chunk.Indexed[A]]
override def from[A](source: IterableOnce[A]): Chunk.Indexed[A] =
if source.knownSize == 0 then indexedEmpty[A]
else
values match
case seq: Chunk.Indexed[A] @unchecked => seq
case seq: IndexedSeq[A] => FromSeq(seq)
case _ => Compact(values.toArray(using ClassTag.Any.asInstanceOf[ClassTag[A]]))
source match
case chunk: Chunk.Indexed[A] @unchecked => chunk
case seq: IndexedSeq[A] => FromSeq(seq)
case _ => Compact(source.iterator.toArray(using ClassTag.Any.asInstanceOf[ClassTag[A]]))
end from

/** Creates a Chunk filled with a specified number of copies of a given value.
/** Creates a new **mutable** builder for constructing Chunks.
*
* @tparam A
* the type of elements in the Chunk
* @param n
* the number of times to repeat the value
* @param v
* the value to fill the Chunk with
* @return
* a new Chunk containing n copies of v
* a mutable Builder that constructs a Chunk[A]
*/
def fill[A](n: Int)(v: A): Chunk[A] =
if n <= 0 then empty
else
val array = (new Array[Any](n)).asInstanceOf[Array[A]]
@tailrec def loop(idx: Int = 0): Unit =
if idx < n then
array(idx) = v
loop(idx + 1)
loop()
Compact(array)
end fill
override def newBuilder[A]: collection.mutable.Builder[A, Chunk[A]] = ChunkBuilder.init[A]

private[kyo] object internal:

Expand Down
70 changes: 70 additions & 0 deletions kyo-data/shared/src/main/scala/kyo/ChunkBuilder.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package kyo

import scala.collection.mutable.ArrayBuilder
import scala.collection.mutable.ReusableBuilder
import scala.reflect.ClassTag

/** A **mutable** builder for creating Chunks.
*
* ChunkBuilder provides an efficient way to construct Chunks by incrementally adding elements. It can be reused after calling result().
*
* @tparam A
* the type of elements in the Chunk being built
*/
sealed abstract class ChunkBuilder[A] extends ReusableBuilder[A, Chunk.Indexed[A]]
object ChunkBuilder:

/** Creates a new ChunkBuilder with no size hint.
*
* @tparam A
* the type of elements in the Chunk to be built
* @return
* a new ChunkBuilder instance
*/
def init[A]: ChunkBuilder[A] = init(0)

/** Creates a new ChunkBuilder with a size hint.
*
* @tparam A
* the type of elements in the Chunk to be built
* @param hint
* the expected number of elements to be added
* @return
* a new ChunkBuilder instance
*/
def init[A](hint: Int): ChunkBuilder[A] =
new ChunkBuilder[A]:
var builder = Maybe.empty[ArrayBuilder[A]]
var _hint = hint

override def addOne(elem: A): this.type =
builder match
case Absent =>
val arr = ArrayBuilder.make[A](using ClassTag.Any.asInstanceOf[ClassTag[A]])
if _hint > 0 then arr.sizeHint(_hint)
arr.addOne(elem)
builder = Maybe(arr)
case Present(arr) => discard(arr.addOne(elem))
end match
this
end addOne

override def sizeHint(n: Int): Unit =
_hint = n
builder.foreach(_.sizeHint(n))

override def clear(): Unit = builder.foreach(_.clear())

override def result(): Chunk.Indexed[A] =
val chunk = builder.fold(Chunk.indexedEmpty[A])(b => Chunk.fromNoCopy(b.result()))
builder.foreach(_.clear())
chunk
end result

override def knownSize: Int = builder.fold(0)(_.knownSize)

override def toString(): String =
if _hint > 0 then s"ChunkBuilder(size = $knownSize, hint = $_hint)"
else s"ChunkBuilder(size = $knownSize)"
end init
end ChunkBuilder
62 changes: 62 additions & 0 deletions kyo-data/shared/src/test/scala/kyo/ChunkBuilderTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package kyo

class ChunkBuilderTest extends Test:

"ChunkBuilder" - {
"empty" in {
val builder = ChunkBuilder.init[Int]
assert(builder.result() == Chunk.empty[Int])
}

"non-empty" in {
val builder = ChunkBuilder.init[Int]
builder.addOne(1)
builder.addOne(2)
builder.addOne(3)
assert(builder.result() == Chunk(1, 2, 3))
}

"clear" in {
val builder = ChunkBuilder.init[Boolean](10)
builder.addOne(true)
builder.clear()
assert(builder.knownSize == 0)
builder.addOne(true)
assert(builder.result() == Chunk(true))
}

"reusable" in {
val builder = ChunkBuilder.init[Int]
builder.addOne(1)
builder.addOne(2)
assert(builder.result() == Chunk(1, 2))
builder.addOne(3)
assert(builder.result() == Chunk(3))
}

"hint" in {
val builder = ChunkBuilder.init[Int](1000)
(0 until 1000).foreach(builder.addOne)
assert(builder.result().length == 1000)
}

"knownSize" in {
val builder = ChunkBuilder.init[Int](20)
assert(builder.knownSize == 0)
(0 until 10).foreach(builder.addOne)
assert(builder.knownSize == 10)
}

"toString" in {
assert(ChunkBuilder.init[Int](10).toString() == "ChunkBuilder(size = 0, hint = 10)")
assert(ChunkBuilder.init[Int].toString() == "ChunkBuilder(size = 0)")
val builder = ChunkBuilder.init[Int]
builder.sizeHint(1)
assert(builder.toString() == "ChunkBuilder(size = 0, hint = 1)")
val hinted = ChunkBuilder.init[Int]
hinted.addOne(1)
hinted.sizeHint(2)
assert(hinted.toString() == "ChunkBuilder(size = 1, hint = 2)")
}
}
end ChunkBuilderTest
9 changes: 8 additions & 1 deletion kyo-data/shared/src/test/scala/kyo/ChunkTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class ChunkTest extends Test:
"creates an empty Chunk from an empty Seq" in {
val seq = Seq.empty[String]
val chunk = Chunk.from(seq)
assert(chunk.isEmpty)
assert(chunk.isEmpty && (chunk eq Chunk.empty))
}

"handles different Seq types" - {
Expand Down Expand Up @@ -508,6 +508,13 @@ class ChunkTest extends Test:
}
}

"to(Chunk)" - {
"converts an Iterable to a Chunk" in {
val iterator = (0 until 5).iterator
assert(iterator.to(Chunk) == Chunk.range(0, 5))
}
}

"toArray" - {
"returns an array with the elements of a Chunk.Compact" in {
val chunk = Chunk(1, 2, 3, 4, 5)
Expand Down

0 comments on commit 5d5f0c7

Please sign in to comment.