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)).Nonemeans no match. -
Option[(T1, T2, ...)]: several sub-values, returned as a tuple (case Grade.Split(from, to)).
Source: Tour of Scala: Extractor Objects
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
Productwith members_1…_N(product match,Npatterns).Productis only a marker here; the compiler never callsproductArityorproductElement; -
any type that has
def isEmpty: Booleananddef get: S.Sis then a single match (one pattern of typeS), or a name-based match ifSitself has members_1…_N(N > 1patterns).
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.
//> 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.unanchoredorfindFirstMatchIn, 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 toNone.
//> 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
forwhose pattern is refutable is an error, unless the pattern is prefixed withcase, which requests the old filtering behaviour (it callswithFilter).
//> 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.
Source: Scala 3.10 release announcement
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
componentNfunctions (see Kotlin Data Classes & Destructuring), andwhenhas 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
OptionorBooleanresults,unapplySeqbound the rest of a sequence asxs @ _*, a refutablevalpattern was accepted without a warning, aforgenerator silently filtered what did not match, and: @uncheckedwas 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.