Extractors & Pattern Bindings

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Scala 3.10 (3.10.0, 2026-10-12) and every example was run with Scala CLI on Scala 3.10.0 and JDK 26 (checked on 2026-10-15). Two items are newer than the 3.3 LTS line: the runtimeChecked method became a stable standard feature in 3.8 (SIP-57), so it exists on the 3.9 LTS and on 3.10 but not on 3.3, and pattern matching on Java records is new in 3.10: the same code fails to compile on 3.9.0. The refutable-pattern checks shown below were introduced in 3.2.

Pattern matching with case classes, Option, List and tuples (see Pattern Matching) works because each of them has an extractor: an object with an unapply or unapplySeq method that the compiler calls to take a value apart. This page explains the contract of those methods, so you can write your own, and then covers the places where patterns appear outside a match: regular expressions, string interpolation, val definitions and for generators.

What an extractor is

An extractor is any object that has an unapply method (or unapplySeq, see below). The pattern Name(p1, p2) calls Name.unapply(scrutinee); if the result says "no match" the next case is tried, otherwise the sub-patterns p1 and p2 are matched against the values it returned. unapply is the inverse of apply, which is why the pair is called construction and deconstruction. A case class gets both generated; a class or an object can define them by hand when the data to match is not stored in the shape you want to expose.

The parameter type of unapply is the type the scrutinee must have. If it is a subtype of the scrutinee’s static type, the compiler performs a type test first and only then calls the method. That is why an extractor over String can be used directly on a value of type Any.

The three result shapes

For an unapply of fixed arity, the Scala 3 reference describes the result type U in terms of two kinds of match. In practice you will use the three classic return types.

//> using scala 3.10.0

object Grade:
  // Boolean extractor: no bindings, only a test
  object Alpine:
    def unapply(g: Int): Boolean = g >= 4

  // single-value extractor: returns Option[T]
  object Digits:
    def unapply(s: String): Option[Int] = s.toIntOption

  // product extractor: returns Option of a tuple
  object Split:
    def unapply(s: String): Option[(String, String)] =
      s.split("->", 2) match
        case Array(a, b) => Some((a.trim, b.trim))
        case _           => None

@main def run(): Unit =
  def describe(n: Int): String = n match
    case Grade.Alpine() => s"$n: alpine"
    case _              => s"$n: not alpine"
  println(describe(5))
  println(describe(2))

  "42" match
    case Grade.Digits(n) => println(s"digits -> ${n + 1}")
    case other           => println(s"no digits in $other")

  "Refugio -> Pico" match
    case Grade.Split(from, to) => println(s"from=$from to=$to")
    case _                     => println("no route")
  assert(Grade.Split.unapply("x").isEmpty)
5: alpine
2: not alpine
digits -> 43
from=Refugio to=Pico
  • Boolean: the pattern takes no arguments (case Grade.Alpine()). It only answers yes or no.

  • Option[T]: one sub-value (case Grade.Digits(n)). None means no match.

  • Option[(T1, T2, ...)]: several sub-values, returned as a tuple (case Grade.Split(from, to)).

Scala 3 extractors beyond Option

Scala 2 only accepted the Boolean, Option and Option-of-tuple shapes (plus unapplySeq). Scala 3 supports a superset, described in the reference page "Option-less pattern matching", so an extractor does not have to allocate an Option and a tuple on every call. The result type U of unapply can be:

  • a Boolean (Boolean match, zero patterns);

  • a subtype of Product with members _1 …​ _N (product match, N patterns). Product is only a marker here; the compiler never calls productArity or productElement;

  • any type that has def isEmpty: Boolean and def get: S. S is then a single match (one pattern of type S), or a name-based match if S itself has members _1 …​ _N (N > 1 patterns).

A product match is always irrefutable. The other forms are refutable unless isEmpty has the singleton type false, which is why Some[T] counts as irrefutable.

flowchart TD U["result type U of unapply"] --> B{"U is Boolean?"} B -->|"yes"| BM["Boolean match: 0 patterns"] B -->|"no"| P{"U extends Product with _1 ... _N?"} P -->|"yes"| PM["product match: N patterns, irrefutable"] P -->|"no"| R{"U has isEmpty and get?"} R -->|"yes"| S{"S = type of get has _1 ... _N with N > 1?"} S -->|"no"| SM["single match: 1 pattern of type S"] S -->|"yes"| NM["name-based match: N patterns"] R -->|"no"| ERR["error: not a valid result type of an unapply method"]
//> using scala 3.10.0

// name-based extractor: the result only needs isEmpty and get, no Option involved
class Doubled(n: Int):
  def isEmpty: Boolean = n <= 0
  def get: Int = n * 2

object Double2:
  def unapply(n: Int): Doubled = Doubled(n)

// product match: the result extends Product (only as a marker) and has _1 and _2 members
class Prefix(s: String) extends Product:
  def _1: String = s.take(3)
  def _2: Int = s.length
  // never called by pattern matching
  def canEqual(that: Any): Boolean = false
  def productArity: Int = 2
  def productElement(n: Int): Any = if n == 0 then _1 else _2

object PrefixOf:
  def unapply(s: String): Prefix = Prefix(s)

// variadic extractor: unapplySeq
object Words:
  def unapplySeq(s: String): Option[Seq[String]] =
    val ws = s.trim.split("\\s+").toSeq
    if ws.head.isEmpty then None else Some(ws)

@main def run(): Unit =
  7 match
    case Double2(d) => println(s"doubled $d")
    case _          => println("not positive")
  -7 match
    case Double2(d) => println(s"doubled $d")
    case _          => println("not positive")
  "Refugio" match
    case PrefixOf(prefix, len) => println(s"$prefix / $len")
  "alpha beta gamma" match
    case Words(first, rest*) => println(s"first=$first rest=$rest")
  "solo" match
    case Words(a) => println(s"one word: $a")
  "a b c d" match
    case Words(a, b, _*) => println(s"$a $b and more")
    case _               => ()
doubled 14
not positive
Ref / 7
first=alpha rest=ArraySeq(beta, gamma)
one word: solo
a b and more

Double2.unapply returns a plain class with isEmpty and get: it never creates an Option. PrefixOf is a product match, so PrefixOf(prefix, len) binds _1 and _2. A class that is neither shape is rejected, for example Prefix without extends Product gives the message “Prefix is not a valid result type of an unapply method of an extractor”.

Variadic extractors: unapplySeq

When the number of sub-values is not fixed, define unapplySeq instead of unapply. Its result is, in the usual case, an Option[Seq[T]]. The pattern may then have any number of element patterns, optionally ending in a sequence wildcard: Words(a), Words(a, b, _*) or Words(first, rest*) in the example above. List, Seq and Array patterns work this way, and so do regular expressions and string interpolators further down. The reference also describes a product-sequence match for a result whose last member is a sequence, for instance an Option[(String, Seq[Int])] that is matched as Foo(name, x, y, ns*).

The preferred way to bind the rest of the sequence in Scala 3 is rest*, as in this complete program. The Scala 2 form rest @ _* still compiles on 3.10 (the last case of the regular-expression example below uses it).

//> using scala 3.10.0
@main def run(): Unit =
  List(1, 2, 3) match
    case List(head, rest*) => println(s"$head $rest")
1 List(2, 3)

(The program prints a "match may not be exhaustive" warning, It would fail on pattern case: Nil, because the only case needs at least one element.)

Extractors with using clauses and nested patterns

unapply may have a leading type clause and any number of using clauses, before or after the value parameter, so an extractor can depend on a context. Extractors can be nested in other patterns, bound with @, and used in guards.

//> using scala 3.10.0

object Always:
  def unapply(n: Int): Some[Int] = Some(n * 2)   // irrefutable

object Maybe:
  def unapply(n: Int): Option[Int] = if n > 0 then Some(n) else None

case class Locale(decimalComma: Boolean)

object Km:
  // an extractor may take a using clause
  def unapply(s: String)(using loc: Locale): Option[Double] =
    val norm = if loc.decimalComma then s.replace(',', '.') else s
    norm.stripSuffix(" km").toDoubleOption

@main def run(): Unit =
  val Always(a) = 5
  println(a)
  val Maybe(b) = 5
  println(b)

  given Locale = Locale(decimalComma = true)
  "7,5 km" match
    case Km(d) => println(d)
    case _     => println("not a distance")

  def f(n: Int) = n match
    case Maybe(x) => x
  println(f(1))
10
5
7.5
1

The compiler also printed this warning for val Maybe(b) = 5, explained in the section on pattern bindings below:

[warn] ./e11.scala:20:7
[warn] pattern binding uses refutable extractor `Maybe`
[warn]
[warn] If this usage is intentional, this can be communicated by adding `.runtimeChecked` after the expression,
[warn] which may result in a MatchError at runtime.
[warn] This patch can be rewritten automatically under -rewrite -source 3.8-migration.
[warn]   val Maybe(b) = 5
[warn]       ^^^^^^^^^^^^

Always returns Some[Int], so val Always(a) = 5 is irrefutable and compiles without any warning. Maybe returns Option[Int], so the same form warns. The match in f does not warn about exhaustivity on this run: the compiler does not reason about the values a hand-written extractor accepts, so it is your job to keep the cases total.

Mistakes the compiler reports

//> using scala 3.10.0

object Digits:
  def unapply(s: String): Option[Int] = s.toIntOption

object Plain

@main def run(): Unit =
  "12" match
    case Digits(a, b) => println(a)
    case _            => ()
  "12" match
    case Plain(a) => println(a)
    case _        => ()
  42 match
    case Digits(n) => println(n)
    case _         => ()
[error] ./e8.scala:10:10
[error] Wrong number of argument patterns for Digits; expected: (Int)
[error]     case Digits(a, b) => println(a)
[error]          ^^^^^^^^^^^^
[error] ./e8.scala:13:10
[error] Plain cannot be used as an extractor in a pattern because it lacks an unapply or unapplySeq method with the appropriate signature
[error]     case Plain(a) => println(a)
[error]          ^^^^^
[error] ./e8.scala:13:30
[error] Not found: a
[error]     case Plain(a) => println(a)
[error]                              ^

The first message is the "Unapply Invalid Number of Arguments" error and the second the "Not An Extractor" error, both listed in the error index. The third message, Not found: a, is only a consequence of the second.

Regular-expression patterns

A scala.util.matching.Regex is itself an extractor: it defines unapplySeq, so a Regex value can appear in a pattern with one sub-pattern per capture group. The binding of each group is a String. Three details cause most surprises.

  • The pattern matches only if the regex matches the whole string. For a search anywhere in the text, use Regex.unanchored or findFirstMatchIn, or put .* around the expression.

  • The number of sub-patterns must equal the number of groups, otherwise the case silently does not match.

  • A group that did not take part in the match is bound to null, not to None.

//> using scala 3.10.0

@main def run(): Unit =
  val Grid = """(\d+)x(\d+)""".r
  "40x25" match
    case Grid(w) => println(s"one group: $w")
    case _       => println("pattern arity differs from group count: no match")
  val Opt = """(\d+)(m)?""".r
  "12" match
    case Opt(n, unit) => println(s"n=$n unit=$unit")
  "12" match
    case Opt(n, null) => println("optional group is null")
  // a regex used as a sequence extractor with a wildcard
  "40x25" match
    case Grid(first, _*) => println(first)
  // findFirstMatchIn for unanchored search
  println(Grid.findFirstMatchIn("grid 40x25 here").map(m => (m.group(1), m.group(2))))
  // unanchored with the Regex helper
  println(Grid.unanchored.unapplySeq("grid 40x25 here"))
  "grid 40x25 here" match
    case Grid.unanchored(w, h) => println(s"unanchored $w $h")
    case _ => ()
pattern arity differs from group count: no match
n=12 unit=null
optional group is null
40
Some((40,25))
Some(List(40, 25))
unanchored 40 25

Wrapping a regex with .* makes it match anywhere, but a greedy .* before the first group takes as much as it can, which is a classic bug. The program below also contains the string interpolation and @ _* cases discussed in the other sections:

//> using scala 3.10.0

@main def run(): Unit =
  val Grid = """(\d+)x(\d+)""".r
  val Elevation = """(?<value>\d+)\s*m""".r
  "40x25" match
    case Grid(w, h) => println(s"w=$w h=$h")
    case _          => println("no grid")
  "1200 m" match
    case Elevation(v) => println(s"elevation $v")
    case _            => ()
  // the regex must match the whole string
  "40x25!" match
    case Grid(_, _) => println("matched")
    case _          => println("no match (whole input must match)")
  // unanchored: use .*
  val Anywhere = """.*(\d+)x(\d+).*""".r
  "grid 40x25 here" match
    case Anywhere(w, h) => println(s"anywhere $w $h")
    case _ => ()
  // string interpolation patterns
  val s"$name is $km km" = "Ridge is 7 km".runtimeChecked
  println(s"$name / $km")
  "Pico: 3200" match
    case s"$peak: $h" if h.toIntOption.isDefined => println(s"peak $peak at ${h.toInt}")
    case _ => println("no")
  // Seq pattern with @ _* (Scala 2 style)
  List(1, 2, 3) match
    case List(head, rest @ _*) => println(s"$head $rest")
    case _ => ()
w=40 h=25
elevation 1200
no match (whole input must match)
anywhere 0 25
Ridge / 7
peak Pico at 3200
1 List(2, 3)

In the Anywhere case the greedy leading .* consumed grid 4 before the first (\d+), so w is 0 instead of 40. Prefer Grid.unanchored as shown above, or a lazy .*?.

String interpolation patterns

The s interpolator can also be used as a pattern, as the s"$name is $km km" and s"$peak: $h" cases above show. Each $x binds a String. An extractor can be placed inside a hole, s"${Digits(h)} m", and a guard narrows the match.

A pattern in a val that can fail, such as val s"$name is $km km" = "Ridge is 7 km", warns like any other refutable pattern definition (see the next section); the program above marks it with .runtimeChecked.

Any interpolator id"..." becomes a pattern when id (reached through an extension on StringContext) returns an object with an unapplySeq method; the compiler calls StringContext(parts*).id.unapplySeq(scrutinee). This is the same mechanism that powers s:

//> using scala 3.10.0

object Digits:
  def unapply(s: String): Option[Int] = s.toIntOption

// a custom interpolator that can also be used as a pattern
class Leg(sc: StringContext):
  def apply(args: Any*): String = sc.s(args*)
  def unapplySeq(s: String): Option[Seq[String]] =
    val regex = sc.parts.map(java.util.regex.Pattern.quote).mkString("(.+?)").r
    regex.unapplySeq(s)

extension (sc: StringContext) def leg: Leg = Leg(sc)

@main def run(): Unit =
  "Refugio-Pico" match
    case leg"$from-$to" => println(s"from $from, to $to")
    case _              => println("no leg")

  println(leg"Refugio-${"Pico"}")
  // extractors nest inside interpolator patterns and anywhere in a pattern
  "3200 m" match
    case s"${Digits(h)} m" => println(s"height ${h + 1}")
    case _                 => println("no height")
  // binder with @, alternatives and an extractor on a part
  List("12", "x", "7").foreach:
    case d @ Digits(n) if n > 10 => println(s"$d is big")
    case Digits(n)               => println(s"$n is small")
    case other                   => println(s"$other is not a number")
from Refugio, to Pico
Refugio-Pico
height 3201
12 is big
x is not a number
7 is small

Because a StringContext carries the literal parts of the pattern (sc.parts), a pattern interpolator can build a regular expression from them as Leg does, or parse the parts in any other way.

Pattern bindings in val definitions and for generators

A pattern can also appear on the left of a val and in a for generator, where there is no case to fall through to. In Scala 2 these were loosely typed: a refutable pattern was accepted, and in a for the elements that did not match were silently filtered out. Scala 3 tightened the rules in 3.2 (earlier under -source future):

  • a pattern definition must be irrefutable, i.e. the type of the right-hand side must conform to the pattern type, otherwise the compiler warns;

  • a generator in a for whose pattern is refutable is an error, unless the pattern is prefixed with case, which requests the old filtering behaviour (it calls withFilter).

//> using scala 3.10.0

case class Trail(name: String, km: Double)

@main def run(): Unit =
  val (a, b) = (1, "two")                // irrefutable: no warning
  val Trail(n, k) = Trail("Ridge", 7.5)  // irrefutable: the pattern covers the type
  println(s"$a $b $n $k")

  val xs = List(10, 20, 30)
  val first :: rest = xs                 // refutable: warning
  println(s"$first $rest")

  val Some(v) = Option(3)                // refutable: warning
  println(v)
[warn] ./e5a.scala:14:17
[warn] pattern's type Some[Int] is more specialized than the right hand side expression's type Option[Int]
[warn]
[warn] If the narrowing is intentional, this can be communicated by adding `.runtimeChecked` after the expression,
[warn] which may result in a MatchError at runtime.
[warn] This patch can be rewritten automatically under -rewrite -source 3.8-migration.
[warn]   val Some(v) = Option(3)                // refutable: warning
[warn]                 ^^^^^^^^^
[warn] ./e5a.scala:11:23
[warn] pattern's type ::[Int] is more specialized than the right hand side expression's type List[Int]
[warn]
[warn] If the narrowing is intentional, this can be communicated by adding `.runtimeChecked` after the expression,
[warn] which may result in a MatchError at runtime.
[warn] This patch can be rewritten automatically under -rewrite -source 3.8-migration.
[warn]   val first :: rest = xs                 // refutable: warning
[warn]                       ^^

The first two bindings are irrefutable: a tuple pattern fits a tuple, and a case-class pattern fits its own type. :: and Some are subtypes of List and Option, so the pattern is "more specialized" than the right-hand side, and it is a warning that the match could throw a MatchError.

For generators the same situation is an error on 3.10:

//> using scala 3.10.0

@main def run(): Unit =
  val pairs = List(Some(1), None, Some(3))
  println(for Some(x) <- pairs yield x)
[error] ./e5b.scala:5:15
[error] pattern's type Some[Int] is more specialized than the right hand side expression's type Option[Int]
[error]
[error] If the narrowing is intentional, this can be communicated by adding the `case` keyword before the full pattern,
[error] which will result in a filtering for expression (using `withFilter`).
[error] This patch can be rewritten automatically under -rewrite -source 3.2-migration.
[error]   println(for Some(x) <- pairs yield x)
[error]               ^^^^^^^

Adding case makes the filtering explicit; this also gives the Scala 2 result for a list of mixed types:

//> using scala 3.10.0

@main def run(): Unit =
  val pairs = List(Some(1), None, Some(3))
  println(for case Some(x) <- pairs yield x)
  val elems: List[Any] = List((1, 2), "hello", (3, 4))
  println(for case (x, y) <- elems yield (y, x))
List(1, 3)
List((2,1), (4,3))

runtimeChecked

When you know that a refutable pattern always matches, for example the head of a list you have already checked, the warning can be silenced by calling runtimeChecked on the right-hand side. It is an extension method in Predef (SIP-57, stable since 3.8 and also available on 3.9 LTS) and it replaces the older : @unchecked ascription. The name says what happens: the compiler skips the checks that can be done at runtime, and a wrong assumption gives a MatchError.

//> using scala 3.10.0

@main def run(): Unit =
  val xs = List(10, 20, 30)
  val first :: rest = xs.runtimeChecked
  println(s"$first $rest")

  val Some(v) = Option(3).runtimeChecked
  println(v)

  val pairs = List(Some(1), None, Some(3))
  println(for case Some(x) <- pairs yield x)

  val empty = List.empty[Int]
  try
    val head :: _ = empty.runtimeChecked
    println(head)
  catch case e: MatchError => println(s"MatchError: ${e.getMessage}")

  // runtimeChecked also silences the exhaustivity check of a match
  sealed trait Shape
  case class Circle(r: Double) extends Shape
  case class Square(s: Double) extends Shape
  val shape: Shape = Circle(2)
  val area = (shape: @unchecked) match
    case Circle(r) => 3.0 * r * r
  println(area)
  val area2 = shape.runtimeChecked match
    case Circle(r) => 3.0 * r * r
  println(area2)
10 List(20, 30)
3
List(1, 3)
MatchError: List() (of class scala.collection.immutable.Nil$)
12.0
12.0

runtimeChecked is not a general mute button. It only switches off checks the compiler can defer to runtime, such as pattern exhaustivity and the narrowing of a pattern definition; unchecked type tests such as case is: ::[Int] on a List[Any] still warn and need Int @unchecked on the type argument. (x: @unchecked) also silences every other pattern-match warning on that scrutinee in Scala 3, while runtimeChecked restores the narrower Scala 2.13 meaning of suppressing only the exhaustivity check. Prefer runtimeChecked, and keep @unchecked for the unsound type-argument case.

Matching Java records

Scala 3.10 can use a Java record as an extractor pattern, including generic records and records with varargs components. A static unapply already defined on the record class takes precedence. For a record compiled in the same project:

public record Waypoint(String name, int elevation) {}
//> using scala 3.10.0

@main def run(): Unit =
  val w = Waypoint("Pico", 3200)
  w match
    case Waypoint(n, e) => println(s"$n at $e m")
  println(w)
Pico at 3200 m
Waypoint[name=Pico, elevation=3200]

Save the two files in one directory and run scala run .. The feature is new in 3.10: on 3.9.0 the same two files fail to compile with Not found: e, because the record has no extractor there.

Compared with Java, Kotlin and Scala 2

  • Java 21 has record deconstruction patterns (see Java Pattern Matching), but a class cannot define its own: only records and sealed hierarchies take part. Scala lets any object be an extractor, with a user-defined, possibly partial, unapply.

  • Kotlin has destructuring declarations built on componentN functions (see Kotlin Data Classes & Destructuring), and when has no custom pattern: a destructuring declaration is positional and cannot fail, while a Scala extractor can fail, so it can drive control flow.

  • Scala 2 extractors needed Option or Boolean results, unapplySeq bound the rest of a sequence as xs @ _*, a refutable val pattern was accepted without a warning, a for generator silently filtered what did not match, and : @unchecked was the escape hatch.

Where the books are outdated

Books written for Scala 2.x show only the Option and Boolean result types, so they present extractors as always allocating, use rest @ _* instead of rest*, and describe val Some(x) = opt and for (Some(x) <- xs) as ordinary code. On Scala 3.10 the first warns and the second does not compile; use .runtimeChecked and for case respectively. They also predate string interpolation patterns for custom interpolators using StringContext extensions, runtimeChecked, and Java record patterns.