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:
| Feature | Chunk | Vector | Array |
|---|---|---|---|
| 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, andslicecreate views without copying - Automatic materialization: Deep operation chains are materialized when depth exceeds thresholds
- Scala collections integration: Implements
IndexedSeqfor 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,flattenappend,prepend,++zip,zipWith,zipWithIndexsorted,sortBy,reversedistinct,materialize
Operations that might produce empty results return Chunk:
filter,filterNotcollecttail,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​
| Operation | Time Complexity | Notes |
|---|---|---|
apply(i) | O(1) | Direct array access for materialized chunks |
length | O(1) | Cached |
head, last | O(1) | |
++ | O(log n) | Balanced tree concatenation |
:+, +: | O(1) amortized | Buffered appends |
take, drop, slice | O(1) | Creates view |
map, filter, flatMap | O(n) | |
updated | O(1) amortized | Buffered updates |
materialize | O(n) | Copies to array |
When to Materialize​
Chunk automatically materializes when:
- Tree depth exceeds internal thresholds
- You call
materializeexplicitly - 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