Scala - Vector with Examples

Vector provides a balanced performance profile across different operations. Unlike List, which excels at head operations but struggles with indexed access, Vector maintains consistent performance for...

Key Insights

  • Vector is Scala’s go-to immutable indexed sequence with effectively constant-time random access and updates, making it ideal for general-purpose collections when you need both fast lookups and efficient modifications
  • Internally implemented as a 32-way branching tree structure (trie), Vector achieves O(log₃₂(n)) performance that’s effectively constant for practical collection sizes up to millions of elements
  • Vector outperforms List for random access and indexed operations while maintaining competitive performance for sequential access, making it the default choice for most indexed collection needs

Understanding Vector’s Performance Characteristics

Vector provides a balanced performance profile across different operations. Unlike List, which excels at head operations but struggles with indexed access, Vector maintains consistent performance for most operations.

import scala.collection.immutable.Vector

// Creating vectors
val empty = Vector.empty[Int]
val numbers = Vector(1, 2, 3, 4, 5)
val range = Vector.range(1, 1000000)

// Random access - effectively O(1)
val element = numbers(2)  // 3
val lastElement = numbers(numbers.length - 1)  // 5

// Prepend and append - effectively O(1)
val prepended = 0 +: numbers  // Vector(0, 1, 2, 3, 4, 5)
val appended = numbers :+ 6   // Vector(1, 2, 3, 4, 5, 6)

The “effectively constant time” claim means operations complete in O(log₃₂(n)) time. For a million elements, that’s only about 4 levels deep in the tree structure, making it practically constant.

Building Vectors Efficiently

When constructing large vectors, use VectorBuilder for better performance than repeatedly appending elements.

// Inefficient approach - creates new vector each iteration
def buildVectorSlow(n: Int): Vector[Int] = {
  var vec = Vector.empty[Int]
  for (i <- 0 until n) {
    vec = vec :+ i
  }
  vec
}

// Efficient approach - uses VectorBuilder
def buildVectorFast(n: Int): Vector[Int] = {
  val builder = Vector.newBuilder[Int]
  for (i <- 0 until n) {
    builder += i
  }
  builder.result()
}

// Even better - use collection operations
val vector = Vector.tabulate(1000)(i => i * 2)
val fromRange = (1 to 1000).toVector
val fromList = List(1, 2, 3, 4, 5).toVector

Updating Elements

Vector’s functional updates create new vectors while sharing most of the internal structure with the original, making updates efficient.

val original = Vector(10, 20, 30, 40, 50)

// Update single element - effectively O(1)
val updated = original.updated(2, 99)
// original: Vector(10, 20, 30, 40, 50)
// updated:  Vector(10, 20, 99, 40, 50)

// Update multiple elements
val multiUpdate = original
  .updated(1, 22)
  .updated(3, 44)
// Vector(10, 22, 30, 44, 50)

// Patch - replace range of elements
val patched = original.patch(1, Vector(100, 200), 2)
// Vector(10, 100, 200, 40, 50)

// Using zipWithIndex for conditional updates
val doubled = original.zipWithIndex.map { 
  case (value, index) if index % 2 == 0 => value * 2
  case (value, _) => value
}
// Vector(20, 20, 60, 40, 100)

Slicing and Partitioning

Vector excels at creating sub-sequences without copying data unnecessarily.

val data = Vector.range(0, 20)

// Take and drop operations
val first5 = data.take(5)           // Vector(0, 1, 2, 3, 4)
val last5 = data.takeRight(5)       // Vector(15, 16, 17, 18, 19)
val without5 = data.drop(5)         // Vector(5, 6, 7, ..., 19)
val middle = data.slice(5, 15)      // Vector(5, 6, 7, ..., 14)

// Split operations
val (left, right) = data.splitAt(10)
// left: Vector(0, 1, ..., 9)
// right: Vector(10, 11, ..., 19)

// Partition by predicate
val (evens, odds) = data.partition(_ % 2 == 0)

// Group consecutive elements
val grouped = data.grouped(3).toVector
// Vector(Vector(0,1,2), Vector(3,4,5), ...)

// Sliding window
val windows = data.sliding(3).toVector
// Vector(Vector(0,1,2), Vector(1,2,3), Vector(2,3,4), ...)

Transformations and Mapping

Vector’s bulk operations leverage structural sharing for efficiency.

val numbers = Vector(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

// Map transformation
val squared = numbers.map(x => x * x)
// Vector(1, 4, 9, 16, 25, 36, 49, 64, 81, 100)

// FlatMap for nested structures
val pairs = numbers.flatMap(x => Vector(x, -x))
// Vector(1, -1, 2, -2, 3, -3, ...)

// Filter operations
val evenNumbers = numbers.filter(_ % 2 == 0)
// Vector(2, 4, 6, 8, 10)

// Collect with partial function
val processed = numbers.collect {
  case x if x % 2 == 0 => x * 10
  case x if x % 3 == 0 => x * 100
}
// Vector(200, 300, 20, 600, 40, 60, 80, 100)

// Scan for cumulative operations
val cumulative = numbers.scan(0)(_ + _)
// Vector(0, 1, 3, 6, 10, 15, 21, 28, 36, 45, 55)

Searching and Querying

Vector provides efficient search operations for indexed sequences.

val items = Vector("apple", "banana", "cherry", "date", "elderberry")

// Index-based search
val index = items.indexOf("cherry")        // 2
val lastIdx = items.lastIndexOf("banana")  // 1

// Predicate-based search
val firstLong = items.find(_.length > 6)   // Some("elderberry")
val indexWhere = items.indexWhere(_.startsWith("c"))  // 2

// Existence checks
val hasDate = items.contains("date")       // true
val hasPattern = items.exists(_.contains("err"))  // true
val allLong = items.forall(_.length > 3)   // true

// Finding with index
val withIndices = items.zipWithIndex
val found = withIndices.find { case (item, _) => item.startsWith("d") }
// Some(("date", 3))

Concatenation and Combining

Efficiently combine vectors using various operations.

val vec1 = Vector(1, 2, 3)
val vec2 = Vector(4, 5, 6)
val vec3 = Vector(7, 8, 9)

// Concatenation
val combined = vec1 ++ vec2 ++ vec3
// Vector(1, 2, 3, 4, 5, 6, 7, 8, 9)

// Interleaving elements
val interleaved = vec1.zip(vec2).flatMap { case (a, b) => Vector(a, b) }
// Vector(1, 4, 2, 5, 3, 6)

// Zip operations
val zipped = vec1.zip(vec2)
// Vector((1,4), (2,5), (3,6))

val zipped3 = vec1.lazyZip(vec2).lazyZip(vec3).toVector
// Vector((1,4,7), (2,5,8), (3,6,9))

// Flatten nested vectors
val nested = Vector(Vector(1, 2), Vector(3, 4), Vector(5, 6))
val flat = nested.flatten
// Vector(1, 2, 3, 4, 5, 6)

Performance Comparison with List

Understanding when to use Vector versus List is crucial for application performance.

import scala.collection.immutable.{List, Vector}

// List excels at head operations
val list = (1 to 1000000).toList
val listHead = list.head                    // O(1) - fast
val listTail = list.tail                    // O(1) - fast
// val listLast = list.last                 // O(n) - slow
// val listRandom = list(500000)            // O(n) - slow

// Vector excels at indexed access
val vector = (1 to 1000000).toVector
val vecHead = vector.head                   // O(1) - fast
val vecLast = vector.last                   // effectively O(1) - fast
val vecRandom = vector(500000)              // effectively O(1) - fast
val vecUpdated = vector.updated(500000, 42) // effectively O(1) - fast

// Use List when:
// - Primarily accessing head/tail
// - Building via recursive prepending
// - Pattern matching on head/tail structure

// Use Vector when:
// - Need random access
// - Frequent updates at arbitrary positions
// - General-purpose indexed sequence
// - Default choice for most scenarios

Vector’s balanced performance characteristics make it the default choice for immutable indexed sequences in Scala. Its effectively constant-time operations across access, updates, and modifications provide predictable performance for collections of practical sizes. Choose Vector unless you have specific requirements that favor List’s head-optimized operations or need the specialized characteristics of other collection types.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.