Skip to content
Tuples and Destructuring
step 1/7

Reading — step 1 of 7

Learn

~3 min readCollections in Depth

Tuples are anonymous records. Combined with destructuring, they're a clean way to return multiple values without defining a case class — and they pair well with pattern matching.

Tuple basics

val point = (3, 4)               // Tuple2[Int, Int]
val triple = (1, "hello", true)  // Tuple3[Int, String, Boolean]

println(point._1)                 // 3
println(point._2)                 // 4
println(triple._3)                // true

Tuples are types Tuple1 through Tuple22. The (a, b, c) syntax is shorthand. Access fields with ._1, ._2, etc. (1-based).

Destructuring assignment

val (x, y) = (3, 4)
println(s"x=$x, y=$y")           // x=3, y=4

val (a, b, c) = computeThree()

Much nicer than tuple._1, tuple._2 everywhere.

Tuples as function returns

Returning multiple values without defining a class:

def divmod(a: Int, b: Int): (Int, Int) = (a / b, a % b)

val (q, r) = divmod(17, 5)
println(s"$q remainder $r")     // 3 remainder 2

For return values that don't have a named class, tuples are concise. For things that are reused or need a domain name (Coordinate, UserId), prefer a case class.

Tuples in pattern matching

def describe(point: (Int, Int)): String = point match {
    case (0, 0) => "origin"
    case (0, _) => "y-axis"
    case (_, 0) => "x-axis"
    case (x, y) if x == y => "diagonal"
    case (x, y) => s"point at ($x, $y)"
}

Tuples destructure naturally inside case patterns. Powerful for state machines and predicate-based dispatch.

Map iteration uses tuples

val ages = Map("Ada" -> 36, "Bob" -> 25)

for ((name, age) <- ages) {
    println(s"$name is $age")
}

ages.foreach { case (name, age) =>
    println(s"$name: $age")
}

Map[K, V] iterates as (K, V) tuples. Destructure in the loop or via case.

zip — pairing collections

val names = List("Ada", "Bob", "Carol")
val ages = List(36, 25, 40)

val paired = names.zip(ages)
// List((Ada, 36), (Bob, 25), (Carol, 40))

paired.map { case (n, a) => s"$n is $a" }

zipWithIndex is a special case:

List("a", "b", "c").zipWithIndex
// List((a, 0), (b, 1), (c, 2))

Useful when you want both element and position in map/filter.

Tuple vs case class

Tuples are faster to write but lose meaning:

// Tuple — anonymous
def parsePoint(s: String): (Int, Int) = ???

// Case class — named
case class Point(x: Int, y: Int)
def parsePoint2(s: String): Point = ???

val p = parsePoint("3,4")
p._1                  // What does _1 mean?

val p2 = parsePoint2("3,4")
p2.x                  // Clear: x coordinate

Rule of thumb:

  • Tuple — short-lived, local, 2-3 values
  • Case class — anything that has a name, is returned across module boundaries, or has more than 3 fields

Common mistakes

  • Tuples beyond 3-4 elements — readability collapses. tuple._5 is meaningless. Use a case class.
  • Tuples in public APIs — caller has to remember positional meaning. Document it or (better) use a named type.
  • Confusing tuple equality with case class(1, 2) == (1, 2) is true (structural). Plain class without case is reference equality.
  • Mixing tuple syntax with pattern syntax in for-loopsfor (k, v <- map) doesn't work; use for ((k, v) <- map) or case (k, v) =>.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…