Skip to main content
Version: 2.x

Chunk

Chunk[A] is an immutable, indexed sequence optimized for high-performance operations. Unlike Scala's built-in collections, Chunk is designed for zero-allocation access patterns, efficient concatenation, and unboxed primitive storage.

Why Chunk?​

Chunk addresses several limitations of standard Scala collections:

FeatureChunkVectorArray
Immutable✓✓✗
O(1) indexed access✓~O(1)✓
Unboxed primitives✓✗✓
Efficient concatenation✓✓✗
Safe functional interface✓✓✗
Lazy slicing✓✗✗

Key advantages:

  • Zero-boxing for primitives: Chunk[Int], Chunk[Double], etc. store values unboxed in specialized arrays
  • Lazy concatenation: Uses balanced tree structures (based on Conc-Trees) for O(log n) concatenation
  • Efficient slicing: drop, take, and slice create views without copying
  • Automatic materialization: Deep operation chains are materialized when depth exceeds thresholds
  • Scala collections integration: Implements IndexedSeq for seamless interoperability

Installation​

Add the following to your build.sbt:

libraryDependencies += "dev.zio" %% "zio-blocks-chunk" % "<version>"

For cross-platform projects (Scala.js):

libraryDependencies += "dev.zio" %%% "zio-blocks-chunk" % "<version>"

Supported Scala versions: 2.13.x and 3.x

Creating Chunks​

From Varargs​

import zio.blocks.chunk.Chunk

val numbers = Chunk(1, 2, 3, 4, 5)
val strings = Chunk("hello", "world")
val empty = Chunk.empty[Int]

From a Single Element​

import zio.blocks.chunk.Chunk

val single = Chunk.single(42)
val unit = Chunk.unit // Chunk(())

From Arrays​

When you have an existing array, use fromArray. Note that the array should not be mutated after wrapping:

import zio.blocks.chunk.Chunk

val arr = Array(1, 2, 3)
val chunk = Chunk.fromArray(arr)

From Iterables and Iterators​

import zio.blocks.chunk.Chunk

val fromList = Chunk.fromIterable(List(1, 2, 3))
val fromVector = Chunk.fromIterable(Vector("a", "b"))
val fromIter = Chunk.fromIterator(Iterator.range(0, 10))

From Java Collections​

import zio.blocks.chunk.Chunk
import java.util

val javaList = new util.ArrayList[String]()
javaList.add("one")
javaList.add("two")

val chunk = Chunk.fromJavaIterable(javaList)

From NIO Buffers​

Chunk provides direct integration with Java NIO buffers:

import zio.blocks.chunk.Chunk
import java.nio.ByteBuffer

val buffer = ByteBuffer.wrap(Array[Byte](1, 2, 3, 4))
val bytes = Chunk.fromByteBuffer(buffer)

Available buffer constructors:

  • Chunk.fromByteBuffer(ByteBuffer): Chunk[Byte]
  • Chunk.fromCharBuffer(CharBuffer): Chunk[Char]
  • Chunk.fromIntBuffer(IntBuffer): Chunk[Int]
  • Chunk.fromLongBuffer(LongBuffer): Chunk[Long]
  • Chunk.fromShortBuffer(ShortBuffer): Chunk[Short]
  • Chunk.fromFloatBuffer(FloatBuffer): Chunk[Float]
  • Chunk.fromDoubleBuffer(DoubleBuffer): Chunk[Double]

Generator Functions​

import zio.blocks.chunk.Chunk

val filled = Chunk.fill(5)("x") // Chunk("x", "x", "x", "x", "x")
val iterated = Chunk.iterate(1, 5)(_ * 2) // Chunk(1, 2, 4, 8, 16)
val unfolded = Chunk.unfold(0)(n => if (n < 5) Some((n, n + 1)) else None)

Core Operations​

Element Access​

import zio.blocks.chunk.Chunk

val chunk = Chunk(10, 20, 30, 40, 50)

val first = chunk(0) // 10
val second = chunk(1) // 20
val head = chunk.head // 10
val last = chunk.last // 50
val len = chunk.length // 5

val maybeHead = chunk.headOption // Some(10)
val maybeLast = chunk.lastOption // Some(50)

For primitive chunks, specialized accessors avoid boxing:

import zio.blocks.chunk.Chunk

val ints = Chunk(1, 2, 3)
val i: Int = ints.int(0) // unboxed access

val bytes = Chunk[Byte](1, 2, 3)
val b: Byte = bytes.byte(0) // unboxed access

val doubles = Chunk(1.0, 2.0, 3.0)
val d: Double = doubles.double(0) // unboxed access

Transformations​

import zio.blocks.chunk.Chunk

val chunk = Chunk(1, 2, 3, 4, 5)

val doubled = chunk.map(_ * 2) // Chunk(2, 4, 6, 8, 10)
val filtered = chunk.filter(_ > 2) // Chunk(3, 4, 5)
val flatted = chunk.flatMap(n => Chunk(n, n)) // Chunk(1, 1, 2, 2, ...)
val collected = chunk.collect { case n if n % 2 == 0 => n * 10 } // Chunk(20, 40)

Concatenation​

Concatenation is efficient—Chunk uses balanced tree structures to avoid copying:

import zio.blocks.chunk.Chunk

val a = Chunk(1, 2, 3)
val b = Chunk(4, 5, 6)

val combined = a ++ b // Chunk(1, 2, 3, 4, 5, 6)
val appended = a :+ 4 // Chunk(1, 2, 3, 4)
val prepended = 0 +: a // Chunk(0, 1, 2, 3)

Slicing​

Slicing operations create views and don't copy data:

import zio.blocks.chunk.Chunk

val chunk = Chunk(1, 2, 3, 4, 5, 6, 7, 8)

val firstThree = chunk.take(3) // Chunk(1, 2, 3)
val lastThree = chunk.takeRight(3) // Chunk(6, 7, 8)
val dropped = chunk.drop(2) // Chunk(3, 4, 5, 6, 7, 8)
val sliced = chunk.slice(2, 5) // Chunk(3, 4, 5)

val (left, right) = chunk.splitAt(4) // (Chunk(1,2,3,4), Chunk(5,6,7,8))

Conditional Operations​

import zio.blocks.chunk.Chunk

val chunk = Chunk(1, 2, 3, 4, 5, 6)

val takeWhileSmall = chunk.takeWhile(_ < 4) // Chunk(1, 2, 3)
val dropWhileSmall = chunk.dropWhile(_ < 4) // Chunk(4, 5, 6)
val takeUntilBig = chunk.takeWhile(_ <= 3) // Chunk(1, 2, 3)
val dropUntilBig = chunk.dropUntil(_ > 3) // Chunk(5, 6)

Folding and Reduction​

import zio.blocks.chunk.Chunk

val chunk = Chunk(1, 2, 3, 4, 5)

val sum = chunk.foldLeft(0)(_ + _) // 15
val product = chunk.foldRight(1)(_ * _) // 120
val summed = chunk.reduce(_ + _) // 15

val runningSum = chunk.foldWhile(0)(_ < 10)(_ + _) // 10 (1+2+3+4)

Searching and Predicates​

import zio.blocks.chunk.Chunk

val chunk = Chunk(1, 2, 3, 4, 5)

val hasEven = chunk.exists(_ % 2 == 0) // true
val allSmall = chunk.forall(_ < 10) // true
val found = chunk.find(_ > 3) // Some(4)
val index = chunk.indexWhere(_ > 3) // 3

Zipping​

import zio.blocks.chunk.Chunk

val as = Chunk("a", "b", "c")
val bs = Chunk(1, 2, 3)

val zipped = as.zip(bs) // Chunk(("a",1), ("b",2), ("c",3))
val withIndex = as.zipWithIndex // Chunk(("a",0), ("b",1), ("c",2))
val zipWith = as.zipWith(bs)(_ + _) // Chunk("a1", "b2", "c3")
val zipAll = as.zipAll(Chunk(1, 2)) // handles different lengths

Updating Elements​

Updates are immutable and use efficient buffering:

import zio.blocks.chunk.Chunk

val chunk = Chunk(1, 2, 3, 4, 5)
val updated = chunk.updated(2, 100) // Chunk(1, 2, 100, 4, 5)

Deduplication and Sorting​

import zio.blocks.chunk.Chunk

val withDupes = Chunk(1, 1, 2, 2, 2, 3, 3)
val deduped = withDupes.dedupe // Chunk(1, 2, 3) - removes adjacent duplicates

val unsorted = Chunk(3, 1, 4, 1, 5)
val sorted = unsorted.sorted // Chunk(1, 1, 3, 4, 5)

Splitting​

import zio.blocks.chunk.Chunk

val chunk = Chunk(1, 2, 3, 4, 5, 6)

val parts = chunk.split(3) // Chunk(Chunk(1,2), Chunk(3,4), Chunk(5,6))
val (before, after) = chunk.splitWhere(_ > 3) // splits at first element > 3

String Conversion​

import zio.blocks.chunk.Chunk
import java.nio.charset.StandardCharsets

val bytes = Chunk[Byte](72, 101, 108, 108, 111)
val str = bytes.asString // "Hello"

val chars = Chunk('H', 'e', 'l', 'l', 'o')
val str2 = chars.asString // "Hello"

val withCharset = bytes.asString(StandardCharsets.UTF_8) // "Hello"
val base64 = bytes.asBase64String // base64-encoded string

Materialization​

For complex operation chains, you can force materialization to an array-backed chunk:

import zio.blocks.chunk.Chunk

val complex = Chunk(1, 2, 3) ++ Chunk(4, 5) ++ Chunk(6, 7)
val materialized = complex.materialize // backed by a single array

NonEmptyChunk​

NonEmptyChunk[A] is a chunk guaranteed to contain at least one element. This enables safe use of operations like head and reduce:

import zio.blocks.chunk.{Chunk, NonEmptyChunk}

val nec = NonEmptyChunk(1, 2, 3)

val first: Int = nec.head // always safe
val sum: Int = nec.reduce(_ + _) // always safe

val mapped: NonEmptyChunk[Int] = nec.map(_ * 2)
val flatMapped: NonEmptyChunk[Int] = nec.flatMap(n => NonEmptyChunk(n, n + 1))

Creating NonEmptyChunk​

import zio.blocks.chunk.{Chunk, NonEmptyChunk}

val fromValues = NonEmptyChunk(1, 2, 3)
val single = NonEmptyChunk.single(42)
val fromCons = NonEmptyChunk.fromCons(::(1, List(2, 3)))
val fromIterable = NonEmptyChunk.fromIterable(1, List(2, 3))

val maybeNec: Option[NonEmptyChunk[Int]] = NonEmptyChunk.fromChunk(Chunk(1, 2))
val empty: Option[NonEmptyChunk[Int]] = NonEmptyChunk.fromChunk(Chunk.empty) // None

Converting Between Chunk and NonEmptyChunk​

import zio.blocks.chunk.{Chunk, NonEmptyChunk}

val nec = NonEmptyChunk(1, 2, 3)
val chunk: Chunk[Int] = nec.toChunk

val chunk2 = Chunk(1, 2, 3)
chunk2.nonEmptyOrElse(0)(_.reduce(_ + _)) // 6 if non-empty, 0 if empty

Operations That Preserve NonEmptiness​

These operations return NonEmptyChunk:

  • map, flatMap, flatten
  • append, prepend, ++
  • zip, zipWith, zipWithIndex
  • sorted, sortBy, reverse
  • distinct, materialize

Operations that might produce empty results return Chunk:

  • filter, filterNot
  • collect
  • tail, init

ChunkBuilder​

ChunkBuilder is a mutable builder for creating chunks efficiently. It's specialized for primitives to avoid boxing:

import zio.blocks.chunk.{Chunk, ChunkBuilder}

val builder = ChunkBuilder.make[Int]()
builder.addOne(1)
builder.addOne(2)
builder.addAll(List(3, 4, 5))
val result: Chunk[Int] = builder.result() // Chunk(1, 2, 3, 4, 5)

Specialized Builders​

For primitives, use specialized builders for best performance:

import zio.blocks.chunk.{Chunk, ChunkBuilder}

val intBuilder = new ChunkBuilder.Int
intBuilder.addOne(1)
intBuilder.addOne(2)
val ints: Chunk[Int] = intBuilder.result()

val byteBuilder = new ChunkBuilder.Byte
val longBuilder = new ChunkBuilder.Long
val doubleBuilder = new ChunkBuilder.Double
val boolBuilder = new ChunkBuilder.Boolean

Available specialized builders: Boolean, Byte, Char, Short, Int, Long, Float, Double

Bit Operations​

Chunk provides efficient bit-level operations for working with binary data:

Converting to Bits​

import zio.blocks.chunk.Chunk

val bytes = Chunk[Byte](0x0F, 0xF0.toByte)
val bits = bytes.asBitsByte // Chunk of 16 booleans

val ints = Chunk(0x12345678)
val intBits = ints.asBitsInt(Chunk.BitChunk.Endianness.BigEndian)

val longs = Chunk(0x123456789ABCDEF0L)
val longBits = longs.asBitsLong(Chunk.BitChunk.Endianness.BigEndian)

Bitwise Operations​

import zio.blocks.chunk.Chunk

val a = Chunk(true, false, true, false)
val b = Chunk(true, true, false, false)

val andResult = a & b // Chunk(true, false, false, false)
val orResult = a | b // Chunk(true, true, true, false)
val xorResult = a ^ b // Chunk(false, true, true, false)
val negated = a.negate // Chunk(false, true, false, true)

Packing Booleans​

import zio.blocks.chunk.Chunk

val bits = Chunk(true, false, true, false, true, true, true, true)
val packedBytes: Chunk[Byte] = bits.toPackedByte // Efficient byte representation

val packedInts: Chunk[Int] = bits.toPackedInt(Chunk.BitChunk.Endianness.BigEndian)
val packedLongs: Chunk[Long] = bits.toPackedLong(Chunk.BitChunk.Endianness.BigEndian)

Binary String​

import zio.blocks.chunk.Chunk

val bits = Chunk(true, false, true, true)
val binary: String = bits.toBinaryString // "1011"

ChunkMap​

ChunkMap[K, V] is an order-preserving immutable map backed by parallel chunks. It maintains insertion order during iteration:

import zio.blocks.chunk.{Chunk, ChunkMap}

val map = ChunkMap("a" -> 1, "b" -> 2, "c" -> 3)

val value = map.get("b") // Some(2)
val updated = map.updated("d", 4)
val removed = map.removed("b")

Creating ChunkMap​

import zio.blocks.chunk.{Chunk, ChunkMap}

val empty = ChunkMap.empty[String, Int]
val fromPairs = ChunkMap("x" -> 1, "y" -> 2)
val fromChunk = ChunkMap.fromChunk(Chunk(("a", 1), ("b", 2)))
val fromChunks = ChunkMap.fromChunks(Chunk("a", "b"), Chunk(1, 2))

Indexed Access​

ChunkMap provides O(1) positional access:

import zio.blocks.chunk.{Chunk, ChunkMap}

val map = ChunkMap("z" -> 1, "a" -> 2, "m" -> 3)

val first = map.atIndex(0) // ("z", 1)
val key = map.keyAtIndex(1) // "a"
val value = map.valueAtIndex(2) // 3

val keys: Chunk[String] = map.keysChunk
val values: Chunk[Int] = map.valuesChunk

Optimized Lookup​

For frequent lookups, create an indexed version with O(1) key access:

import zio.blocks.chunk.ChunkMap

val map = ChunkMap("a" -> 1, "b" -> 2, "c" -> 3)
val indexed = map.indexed // O(1) lookups, extra memory for index

val value = indexed.get("b") // O(1) instead of O(n)

Scala Collections Integration​

Chunk implements IndexedSeq and integrates seamlessly with Scala collections:

import zio.blocks.chunk.Chunk

val chunk = Chunk(1, 2, 3, 4, 5)

val list: List[Int] = chunk.toList
val vector: Vector[Int] = chunk.toVector
val array: Array[Int] = chunk.toArray

val fromSeq: Chunk[Int] = Chunk.from(Vector(1, 2, 3))

Standard collection operations work as expected:

import zio.blocks.chunk.Chunk

val chunk = Chunk(1, 2, 3)
val result = chunk
.filter(_ > 1)
.map(_ * 2)
.flatMap(n => Chunk(n, n + 1))

Performance Characteristics​

OperationTime ComplexityNotes
apply(i)O(1)Direct array access for materialized chunks
lengthO(1)Cached
head, lastO(1)
++O(log n)Balanced tree concatenation
:+, +:O(1) amortizedBuffered appends
take, drop, sliceO(1)Creates view
map, filter, flatMapO(n)
updatedO(1) amortizedBuffered updates
materializeO(n)Copies to array

When to Materialize​

Chunk automatically materializes when:

  • Tree depth exceeds internal thresholds
  • You call materialize explicitly
  • Converting to array with toArray

Consider explicit materialization when:

  • Performing many random accesses on a deeply nested chunk
  • Passing data to APIs that need arrays
  • Optimizing a hot loop