Pattern Matching
|
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, released 2026-10-12) and every example was compiled and run with Scala 3.10.0 through Scala CLI on JDK 26 (checked on 2026-10-15). The 3.9 LTS line (3.9.0, 2026-09-03) has the same core pattern matching, because the core has not changed since Scala 3.0; the differences that matter are called out in the text and collected in Scala 3.9 LTS versus 3.10. Examples were run on 3.10.0 only, so statements about 3.9 are taken from the release notes, not from a run.
Pattern matching is Scala’s way of asking "which shape does this value have?" and taking it apart in the same step.
A match expression compares a value, the scrutinee, against a list of case clauses from top to bottom. The first
clause whose pattern matches and whose guard (if any) is true supplies the result. Because match is an expression it
has a type, and because the compiler knows the shape of sealed types, enum`s and case classes, it can tell you when
you forgot a case. The examples use the trailhead scenario, a toolkit that plans hikes on a network of mountain trails.
For the type-driven side of the story see Type Hierarchy & Matchable; for custom
patterns and patterns in `val definitions see Extractors & Pattern Bindings;
for the types being matched see Enums and
Case Classes.
match as an expression
A match produces the value of the selected clause, so it can be assigned, returned or passed on without a temporary
variable. In Scala 3 the clauses are indented under the scrutinee and the braces are optional (see
Optional Braces & Indentation). The clause arrow is =>, and the
result type is the least upper bound of the clause bodies.
The first example uses most of the basic pattern kinds on a value of type Any.
//> using scala 3.10.0
package trailhead
enum Difficulty:
case Easy, Moderate, Hard
case Alpine(grade: Int)
case class Trail(name: String, lengthKm: Double, ascentM: Int, difficulty: Difficulty)
def describe(x: Any): String = x match
case 0 => "zero"
case "Pico" => "the summit"
case 'a' => "the letter a"
case n: Int if n < 0 => s"negative int $n"
case n: Int => s"int $n"
case s: String => s"string of length ${s.length}"
case (a, b) => s"pair of $a and $b"
case _: Trail => "a trail"
case other => s"something else: $other"
def label(d: Difficulty): String = d match
case Difficulty.Easy | Difficulty.Moderate => "family friendly"
case Difficulty.Hard => "experienced only"
case Difficulty.Alpine(g) if g >= 4 => s"serious alpine, grade $g"
case Difficulty.Alpine(g) => s"easy alpine, grade $g"
@main def run(): Unit =
List[Any](0, "Pico", 'a', -3, 7, "Refugio", (1, "x"), Trail("Ridge", 8.5, 900, Difficulty.Hard), 2.5).foreach: v =>
println(describe(v))
List(Difficulty.Easy, Difficulty.Hard, Difficulty.Alpine(5), Difficulty.Alpine(2)).foreach(d => println(label(d)))
zero
the summit
the letter a
negative int -3
int 7
string of length 7
pair of 1 and x
a trail
something else: 2.5
family friendly
experienced only
serious alpine, grade 5
easy alpine, grade 2
Source: Scala 3 Book: Match expressions
The pattern kinds shown, in the order in which they appear in the example, are:
| Pattern | Example | Matches |
|---|---|---|
Literal (constant) |
|
A value equal to the literal, tested with |
Variable |
|
Anything. A lowercase name that is not backquoted introduces a new binding. |
Wildcard |
|
Anything, with no binding. |
Typed |
|
A value whose runtime class conforms to the type. The binding has that type. |
Tuple |
|
A |
Constructor |
|
An instance built by a case class, an enum case or any object with a matching |
Alternative |
|
Any of the alternatives, provided none binds a variable. |
The order matters. The compiler warns about clauses that can never be reached, and it is a good idea to put the specific clauses before the general ones.
Constructor patterns, alternatives and guards
The constructor patterns of case classes and enum cases are the workhorse of Scala code. They nest to any depth, they bind
parts of the value, and they can be combined with alternatives (patterns separated by a vertical bar) and with guards
(a boolean condition after if). A guard sees the variables bound by its pattern, and a failing guard makes the match
fall through to the next clause.
The label method of the first example already combines the three:
def label(d: Difficulty): String = d match
case Difficulty.Easy | Difficulty.Moderate => "family friendly"
case Difficulty.Hard => "experienced only"
case Difficulty.Alpine(g) if g >= 4 => s"serious alpine, grade $g"
case Difficulty.Alpine(g) => s"easy alpine, grade $g"
An alternative pattern may not bind variables, because the compiler could not know which of the alternatives supplied the value. The error is real, and it applies even when every alternative binds the same name.
//> using scala 3.10.0
def f(x: Any) = x match
case Some(n) | Some(m) => 1
case _ => 0
@main def run(): Unit = println(f(1))
[error] ./a1.scala:3:13
[error] Illegal variable n in pattern alternative
[error] case Some(n) | Some(m) => 1
[error] ^
[error] ./a1.scala:3:23
[error] Illegal variable m in pattern alternative
[error] case Some(n) | Some(m) => 1
[error] ^
An alternative can be given a name as a whole with a binder, which is the subject of the next section. Alternatives are
also allowed inside nested patterns, as in case Some(1 | 2), and in an anonymous function written with cases:
//> using scala 3.10.0
def f(x: Int): Int = x match
case 1 | 2 => 10
case n if n > 100 => 100
case n @ (3 | 4 | 5) => n * 2
case _ => 0
val classify: Int => String =
case 0 => "zero"
case n if n % 2 == 0 => "even"
case _ => "odd"
@main def run(): Unit =
println(List(1,3,200,7).map(f))
println(List(0,1,2).map(classify))
println(List(Some(1), None, Some(3)).collect { case Some(n) => n })
List(10, 6, 100, 0)
List(zero, odd, even)
List(1, 3)
Source: Tour of Scala: Pattern guards
The val classify: Int => String above shows that a sequence of case clauses is itself a function literal.
Passing { case ... } to collect, map or foreach is the idiomatic way to destructure arguments. A function literal
written with case clauses that covers only some inputs is a partial function; see
Partial Functions.
A variable may appear only once in a pattern, and the compiler enforces it:
//> using scala 3.10.0
def f(x: (Int, Int)) = x match
case (a, a) => 1
@main def run(): Unit = println(f((1,2)))
[error] ./a2.scala:3:12
[error] duplicate pattern variable: a
[error] case (a, a) => 1
[error] ^
Sequence, tuple and binder patterns
Sequences are matched through the unapplySeq method that List, Vector, Seq and Array provide in their companion
objects. List(a, b) matches a list of exactly two elements. A trailing rest* captures the remaining elements as a
Seq, and a trailing _* ignores them. The :: infix pattern is the constructor of a non-empty list, so
first :: _ :: rest matches a list of at least two elements and binds the head and the tail of the tail. Tuple patterns
are just constructor patterns with the same arity.
An @ binder, as in all @ pattern, binds the whole matched value while still testing the sub-pattern. This avoids
rebuilding a value that was just taken apart.
//> using scala 3.10.0
def summarize(stops: List[String]): String = stops match
case Nil => "no stops"
case List(only) => s"one stop: $only"
case List(a, b) => s"two stops: $a and $b"
case first :: _ :: rest if rest.nonEmpty =>
s"starts at $first, ${rest.length + 1} more stops"
case _ => "unreachable"
def ends(xs: Seq[Int]): String = xs match
case Seq() => "empty"
case Seq(single) => s"single=$single"
case Seq(first, rest*) => s"first=$first, ${rest.length} more, rest=$rest"
val Summit = "Pico"
val summit = "Pico"
def upper(s: String): String = s match
case Summit => "uppercase name: compared with =="
case other => s"other: $other"
def lower(s: String): String = s match
case `summit` => "backquoted name: compared with =="
case other => s"other: $other"
def pair(p: (String, Int)): String = p match
case ("Refugio", beds) => s"hut with $beds beds"
case (name, n) if n > 1000 => s"$name is high ($n m)"
case (name, _) => s"$name"
@main def run(): Unit =
println(summarize(Nil))
println(summarize(List("A")))
println(summarize(List("A", "B")))
println(summarize(List("A", "B", "C", "D")))
println(ends(Vector(1, 2, 3, 4)))
println(ends(Vector(1, 2)))
println(ends(List(9)))
println(upper("Pico"))
println(upper("Valle"))
println(lower("Pico"))
println(lower("Valle"))
println(pair(("Refugio", 40)))
println(pair(("Aneto", 3404)))
println(pair(("Valle", 900)))
val route = List(("Pico", 2800), ("Refugio", 2100))
val high = route match
case all @ (("Pico", _) :: _) => s"starts at the summit: ${all.size} stops"
case _ => "other"
println(high)
val named = Some(List(1, 2, 3)) match
case Some(xs @ List(1, _*)) => s"list starting with 1: $xs"
case _ => "other"
println(named)
no stops
one stop: A
two stops: A and B
starts at A, 3 more stops
first=1, 3 more, rest=Vector(2, 3, 4)
first=1, 1 more, rest=Vector(2)
single=9
uppercase name: compared with ==
other: Valle
backquoted name: compared with ==
other: Valle
hut with 40 beds
Aneto is high (3404 m)
Valle
starts at the summit: 2 stops
list starting with 1: List(1, 2, 3)
Source: Scala 3 Book: Binding matched patterns to variables, Tour of Scala: Binding matched patterns to variables
Three rules in that example deserve attention.
Uppercase names are stable identifiers. A pattern that starts with an uppercase letter, such as Summit, is not a new
variable: it refers to the existing value and the scrutinee is compared with it using ==. A lowercase name is a
variable binding. To compare against an existing lowercase val you must backquote it, as in case `summit` =>, which turns it
into a stable-identifier pattern. Forgetting the backquotes is a classic mistake: the clause then matches everything and
shadows the clauses that follow, and the compiler says so.
//> using scala 3.10.0
val summit = "Pico"
def lower(s: String): String = s match
case summit => "matched"
case other => s"other: $other"
@main def run(): Unit = println(lower("Valle"))
[warn] ./bad1.scala:5:8
[warn] Unreachable case
[warn] case other => s"other: $other"
[warn] ^^^^^
The same constant could be matched with a literal pattern, but a stable identifier lets you name the constant, and works for
any val, object or enum case that is a stable path.
The sequence wildcard has to come last. A pattern such as Seq(first, middle*, last) is rejected, because the matched
elements are taken from the front and only the tail can be variable-length:
//> using scala 3.10.0
def f(xs: Seq[Int]) = xs match
case Seq(first, middle*, last) => first + last
case _ => 0
@main def run(): Unit = println(f(Seq(1,2,3)))
[error] ./bad2.scala:3:25
[error] spread operator `*` not allowed here; must come last in a parameter list
[error] case Seq(first, middle*, last) => first + last
[error] ^
Match a "prefix and suffix" shape with the +: and :+ extractors, for example the pattern
first +: _ :+ last, or by guards on size.
The sequence splice in patterns is written rest* and is the form to prefer. The older rest @ _* and
_* forms are still accepted on 3.10 (a test compile showed no warning) and are what Scala 2 code uses. In
expressions the old form xs: _* has produced a deprecation warning since 3.4.
Exhaustivity and sealed types
The compiler checks every match on a sealed type, an enum or a case-class hierarchy for exhaustivity. If some value
can reach the end without matching any clause, you get a warning that names a pattern the match would fail on. This is
the main reason to prefer sealed hierarchies and `enum`s to open ones: adding a new case turns every match that needs
updating into a warning.
//> using scala 3.10.0
enum Segment:
case Path, Ridge
case River(ford: Boolean)
case Hut(beds: Int)
import Segment.*
def effort(s: Segment): Int = s match
case Path => 1
case Ridge => 3
case River(true) => 2
case Hut(_) => 0
@main def run(): Unit = println(effort(Ridge))
[warn] ./ex1.scala:9:31
[warn] match may not be exhaustive.
[warn]
[warn] It would fail on pattern case: Segment.River(false)
[warn] def effort(s: Segment): Int = s match
[warn] ^
Source: Tour of Scala: Sealed types
The warning is reported as "match may not be exhaustive", and the Reference documents it as error code E029
(Pattern Match Exhaustivity). It is a warning, not an error, unless you compile with -Werror (or -Xfatal-warnings on
older versions). Scala CLI prints the message without the code header that scalac shows. The checker is
conservative about guards: it assumes any clause with a guard might not match, so it asks for a clause without a guard
(a final case _) even when the guards cover all values.
Without a clause that matches, the failure surfaces at run time as scala.MatchError, whose message contains the
rendered value and its class. The @unchecked annotation on the scrutinee tells the compiler to skip the check, and
@switch asks it to verify that the match compiles to a JVM tableswitch or lookupswitch.
//> using scala 3.10.0
import scala.annotation.switch
enum Segment:
case Path, Ridge
case River(ford: Boolean)
def risky(s: Segment): Int = (s: @unchecked) match
case Segment.Path => 1
case Segment.Ridge => 3
def fast(n: Int): String = (n: @switch) match
case 1 => "one"
case 2 => "two"
case 3 => "three"
case _ => "many"
@main def run(): Unit =
println(fast(2))
try println(risky(Segment.River(true)))
catch case e: MatchError => println(s"MatchError: ${e.getMessage}")
two
MatchError: River(true) (of class Segment$River)
Source: Scala API: scala.MatchError
Use @unchecked sparingly. It documents a promise you make to the compiler, and if the promise is wrong you get the
MatchError instead of a compile-time warning. A complement is the unreachable-clause warning: a clause that an earlier
clause already covers is flagged, and this includes clauses placed after a variable pattern or after a total constructor
pattern. The reference lists it as E030 (Match Case Unreachable).
//> using scala 3.10.0
sealed trait Shape
case class Circle(r: Double) extends Shape
case class Square(s: Double) extends Shape
def area(sh: Shape): Double = sh match
case Circle(r) => 3.0 * r * r
case Square(s) => s * s
case Circle(0) => 0.0
def area2(sh: Shape): Double = (sh: @unchecked) match
case Circle(r) => 3.0 * r * r
@main def run(): Unit = println(area2(Circle(1)))
[warn] ./ex2.scala:9:8
[warn] Unreachable case
[warn] case Circle(0) => 0.0
[warn] ^^^^^^^^^
Source: Tour of Scala: Sealed types
The second clause, Circle(0), can never run because Circle(r) already covers every circle. Reorder the clauses so
that the specific one comes first. The match with @unchecked in area2 is accepted with no warning about Square.
Chained and postfix match
The syntax of match changed in Scala 3: it now behaves like an alphabetic infix operator, and that has two
consequences. match expressions can be chained, so the result of one match is the scrutinee of the next, and match
can follow a period, as in xs.match ..., which lets you test a value without a temporary name in the middle of an
if or after a method chain. The scrutinee must be an infix expression; the old form x: T match with a type ascription
no longer works and must be written (x: T) match.
//> using scala 3.10.0
def count(xs: List[String]): Int =
xs match
case Nil => "empty"
case _ => "nonempty"
match
case "empty" => 0
case "nonempty" => 1
def count2(xs: List[String]): Int =
xs.match
case Nil => false
case _ => true
.match
case true => 1
case false => 0
def kind(xs: List[Int]): String =
if xs.match
case Nil => false
case _ => true
then "nonempty"
else "empty"
@main def run(): Unit =
println(count(Nil))
println(count(List("a")))
println(count2(Nil))
println(kind(List(1)))
println(kind(Nil))
0
1
0
nonempty
empty
Chained matches are rarely necessary in everyday code, and a named intermediate value is usually clearer. The postfix form is useful when the scrutinee is the end of a long method chain.
Type patterns, erasure and Matchable
A typed pattern, n: Int, tests the runtime class of the value. The JVM erases type arguments, so a pattern can test
the class List but not the element type. The compiler tells you when a type test cannot be checked at run time.
//> using scala 3.10.0
def f(x: Any): String = x match
case xs: List[Int] => "list of Int"
case _ => "other"
@main def run(): Unit =
println(f(List("a")))
[warn] ./ty1.scala:3:8
[warn] the type test for List[Int] cannot be checked at runtime because its type arguments can't be determined from Any
[warn] case xs: List[Int] => "list of Int"
[warn] ^
The program prints list of Int for a list of strings. The test was reduced to List, so the unchecked pattern is a bug
waiting for a ClassCastException further down the code. The remedies are, in order of preference:
-
Match on a type whose arguments are determined by the scrutinee’s type. If
xis aBox[A], a patternx: IntBoxis checkable, becauseIntBoxextendsBox[Int]. -
Use wildcard arguments,
List[?],Map[?, ?], when you only care about the class. Primitive arrays are fine, becauseArray[Int]is a distinct runtime class. -
Ask for a
scala.reflect.TypeTest[Any, T](or aClassTag[T]) as a context parameter when the type is a type parameter of the method. ATypeTestmakes a pattern on an abstract typeTsafe, because the caller passes in the evidence that is synthesised for it.
//> using scala 3.10.0
def f(x: Any): String = x match
case xs: List[?] => s"a list of ${xs.size}"
case m: Map[?, ?] => s"a map of ${m.size}"
case a: Array[Int] => s"ints ${a.length}"
case _ => "other"
def g[T](x: Any)(using tt: scala.reflect.TypeTest[Any, T]): Option[T] = x match
case t: T => Some(t)
case _ => None
@main def run(): Unit =
println(f(List("a")))
println(f(Map(1 -> 2)))
println(f(Array(1, 2)))
println(g[String]("x"))
println(g[String](1))
a list of 1
a map of 1
ints 2
Some(x)
None
Source: Scala 3 Reference: TypeTest
Without the TypeTest the pattern case t: T on an unconstrained type parameter produces the warning "the type test for T
cannot be checked at runtime because it refers to an abstract type member or type parameter", and the clause matches any
value.
Matchable
Scala 3 adds the trait Matchable, the supertype of the values that can be pattern matched. Any is not Matchable,
so that a match cannot look through an opaque type and expose its underlying representation. A match on a selector
statically typed Any is therefore reported, but only under -source:future:
//> using scala 3.10.0
//> using option -source:future
def describe(x: Any): String = x match
case s: String => s"string $s"
case i: Int => s"int $i"
case _ => "other"
@main def run(): Unit = println(describe("a"))
[warn] ./mt2.scala:5:11
[warn] pattern selector should be an instance of Matchable,
[warn] but it has unmatchable type Any instead
[warn] case s: String => s"string $s"
[warn] ^^^^^^
[warn] ./mt2.scala:6:11
[warn] pattern selector should be an instance of Matchable,
[warn] but it has unmatchable type Any instead
[warn] case i: Int => s"int $i"
[warn] ^^^
The fix is to convert the selector with asMatchable (imported from scala.compiletime) at the place where you take the
value apart:
//> using scala 3.10.0
//> using option -source:future
import scala.compiletime.asMatchable
def f(x: Any): String = x.asMatchable match
case s: String => s"string $s"
case i: Int => s"int $i"
case _ => "other"
@main def run(): Unit = println(f("a"))
string a
By default (without -source:future) all the earlier examples that match on Any compile with no warning. See
Type Hierarchy & Matchable for the design.
Strict equality and pattern matching (SIP-67)
Matching against an enum case or a case object without arguments is implemented with ==. Under the opt-in
import scala.language.strictEquality, == between two values needs a CanEqual instance. Until 3.10 that also applied
to a plain match, which made strict equality painful to adopt: a type that cannot or should not derive CanEqual,
for example because one of its cases holds a function, could not be matched on at all. The SIP page describes the error that
you used to get from a match against Nat.Zero, "Values of types Nat and Nat cannot be compared with == or !=".
SIP-67 (strict-equality pattern matching) removes the requirement when the pattern is an object that extends the
scrutinee’s type, or an enum case without a parameter list. It was experimental from Scala 3.8 and is stable in 3.10, so on
the 3.9 LTS line it is still experimental and a match like the one below may need the CanEqual instance. Equality
tests with == stay strict.
//> using scala 3.10.0
import scala.language.strictEquality
enum Segment:
case Path
case Hut(beds: Int)
case Cable(speed: Int => Int)
def effort(s: Segment): Int = s match
case Segment.Path => 1
case Segment.Hut(b) => b
case Segment.Cable(f) => f(10)
@main def run(): Unit =
println(effort(Segment.Path))
println(effort(Segment.Cable(_ * 2)))
1
20
The == that the SIP deliberately keeps illegal is still reported:
//> using scala 3.10.0
import scala.language.strictEquality
enum Segment:
case Path
case Cable(speed: Int => Int)
def same(a: Segment, b: Segment): Boolean = a == b
@main def run(): Unit = println(same(Segment.Path, Segment.Path))
[error] ./se1.scala:8:45
[error] Values of types Segment and Segment cannot be compared with == or !=
[error] def same(a: Segment, b: Segment): Boolean = a == b
[error] ^^^^^^
Source: Scala 3.10 release notes
Sub-cases (experimental)
A case can be followed by a nested match, introduced by if. This is the sub-cases feature. The sub-match runs only
when the outer pattern matches, and if none of its cases matches, control falls through to the next outer clause, as if the
outer clause had not matched. That is the difference from a plain nested match in the body, which commits to the outer
case and needs its own fallback. It is experimental: it needs import scala.language.experimental.subCases, and its
syntax is not covered by the compatibility guarantees of stable features. Without the import the compiler rejects it:
//> using scala 3.10.0
enum Difficulty:
case Easy, Hard
case Alpine(grade: Int)
case class Trail(name: String, difficulty: Difficulty)
def advice(t: Option[Trail]): String = t match
case Some(x) if x.difficulty match
case Difficulty.Alpine(g) if g >= 4 => s"${x.name}: bring a rope"
case Difficulty.Easy => s"${x.name}: sandals will do"
case _ => "no special advice"
@main def run(): Unit =
println(advice(Some(Trail("Pico", Difficulty.Alpine(5)))))
println(advice(Some(Trail("Valle", Difficulty.Easy))))
println(advice(Some(Trail("Cresta", Difficulty.Alpine(2)))))
println(advice(None))
[error] ./sub1.scala:12:3
[error] '=>' expected, but 'case' found
[error] case _ => "no special advice"
[error] ^^^^
[error] ./sub1.scala:9:19
[error] Found: Unit
[error] Required: String
With the import it compiles and falls through as described:
//> using scala 3.10.0
import scala.language.experimental.subCases
enum Difficulty:
case Easy, Hard
case Alpine(grade: Int)
case class Trail(name: String, difficulty: Difficulty)
def advice(t: Option[Trail]): String = t match
case Some(x) if x.difficulty match
case Difficulty.Alpine(g) if g >= 4 => s"${x.name}: bring a rope"
case Difficulty.Easy => s"${x.name}: sandals will do"
case _ => "no special advice"
@main def run(): Unit =
println(advice(Some(Trail("Pico", Difficulty.Alpine(5)))))
println(advice(Some(Trail("Valle", Difficulty.Easy))))
println(advice(Some(Trail("Cresta", Difficulty.Alpine(2)))))
println(advice(None))
Pico: bring a rope
Valle: sandals will do
no special advice
no special advice
Sub-cases are supported in match, catch and partial functions, and the exhaustivity checker conservatively treats them
as partial, like guards. The feature is listed as experimental in the 3.10 Reference. It is experimental on 3.9.0 too: the example above, with
the same import scala.language.experimental.subCases, compiles and prints the same four lines on 3.9.0, and without the
import it does not compile.
Putting it together
The trailhead planner converts segments to walking times and checks a route for dangerous shapes. It uses constructor
patterns with nested literals, a guard-free exhaustive match, the :+ extractor for the last element of a list, a
list prefix and a final guard.
//> using scala 3.10.0
package trailhead
enum Segment:
case Path(km: Double)
case Ridge(km: Double, exposed: Boolean)
case River(km: Double, ford: Boolean)
case Hut(beds: Int)
import Segment.*
/** Naismith's rule, simplified: 12 minutes per km, plus a penalty per kind of segment. */
def minutes(s: Segment): Int = s match
case Path(km) => (km * 12).toInt
case Ridge(km, true) => (km * 12).toInt + 20
case Ridge(km, false) => (km * 12).toInt
case River(_, false) => 90
case River(km, true) => (km * 12).toInt + 10
case Hut(_) => 0
def warning(route: List[Segment]): Option[String] = route match
case _ :+ River(_, false) => Some("the last segment is an unfordable river")
case Ridge(_, true) :: Ridge(_, true) :: _ => Some("two exposed ridges at the start")
case _ if route.contains(Hut(0)) => Some("a hut with no beds")
case _ => None
@main def plan(): Unit =
val route = List(Ridge(2.0, true), Ridge(1.5, true), Hut(12), River(0.4, true))
println(route.map(minutes))
println(warning(route))
println(warning(List(Path(1.0), River(0.3, false))))
println(warning(List(Path(1.0), Hut(0))))
println(warning(List(Path(1.0))))
List(44, 38, 0, 14)
Some(two exposed ridges at the start)
Some(the last segment is an unfordable river)
Some(a hut with no beds)
None
Scala 2 and Java comparison
| Topic | Scala 2 | Scala 3 |
|---|---|---|
Splice in a pattern |
|
|
Chained match and |
Not available. |
Available, because |
Scrutinee with a type ascription |
|
Write |
|
Must be |
Option-less extractors are allowed (Boolean, product, single and name-based matches). See Extractors & Pattern Bindings. |
Strict equality and enums |
|
Opt-in |
Braces |
Required around the cases. |
Optional, with indentation. |
In Java, pattern matching for instanceof and switch (final in Java 21, JEP 441) and record patterns (JEP 440) cover
the same ground for sealed interfaces and records. The main differences are that Scala patterns are also extractor-based,
so any class can offer its own unapply, and that Scala sequence, alternative and @ binders have no Java counterpart.
Java’s switch over an enum or sealed hierarchy is checked for exhaustivity, as in Scala, but
Java guards are written when, where Scala uses if. Kotlin’s when tests constants, ranges and is types and smart
casts the value, but it has no nested constructor patterns; the Kotlin equivalent of a Scala nested pattern is a chain of
is tests and property accesses. See Kotlin Sealed Classes and Enums for the Kotlin side.
Scala 3.9 LTS versus 3.10
-
Constant, variable, typed, constructor, sequence, tuple, alternative and binder patterns, guards, exhaustivity checks, chained
matchandTypeTesthave been part of Scala 3 since 3.0 and are the same on the 3.9 LTS and 3.10. -
SIP-67 strict-equality pattern matching is stable in 3.10. It was experimental in 3.8 and 3.9 (release notes), so on the LTS line a
matchon an enum or object understrictEqualitymay still need aCanEqualinstance. -
Sub-cases are experimental on both 3.9 and 3.10, behind the same
subCasesimport. -
The
Matchablewarning for a pattern selector of typeAnyis still-source:futureonly on 3.10.
Where the books are outdated
Books written for Scala 2 or for the first Scala 3 releases show the following forms. The current form is the one used in this page.
-
rest @ _*andxs: _*splices. Preferrest*; the expression formxs: _*has been deprecated since 3.4. -
Extractors that must return
Option. Scala 3 also accepts Boolean, product, single and name-based results. -
Pattern matching on
Anywithout discussingMatchable. It matters only under-source:future, but it explains whyAnyis not the right type for open-ended values. -
x: T match ...and the lack of chainedmatch. Scala 3 requires parentheses around an ascribed scrutinee. -
Braces around the
caseclauses. They are optional. -
No mention of
strictEquality(Scala 3.0) or of SIP-67 and sub-cases (Scala 3.8 to 3.10).