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

The pattern kinds shown, in the order in which they appear in the example, are:

Pattern Example Matches

Literal (constant)

0, "Pico", 'a'

A value equal to the literal, tested with ==. Integer, floating-point, character, string, boolean and null literals are allowed.

Variable

other

Anything. A lowercase name that is not backquoted introduces a new binding.

Wildcard

_

Anything, with no binding.

Typed

n: Int, _: Trail

A value whose runtime class conforms to the type. The binding has that type.

Tuple

(a, b)

A Tuple2, with each component matched by the sub-pattern.

Constructor

Difficulty.Alpine(g)

An instance built by a case class, an enum case or any object with a matching unapply, with each argument matched by a sub-pattern.

Alternative

Easy or Moderate joined by a vertical bar

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)

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)

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]                               ^

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)

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]        ^^^^^^^^^

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 x is a Box[A], a pattern x: IntBox is checkable, because IntBox extends Box[Int].

  • Use wildcard arguments, List[?], Map[?, ?], when you only care about the class. Primitive arrays are fine, because Array[Int] is a distinct runtime class.

  • Ask for a scala.reflect.TypeTest[Any, T] (or a ClassTag[T]) as a context parameter when the type is a type parameter of the method. A TypeTest makes a pattern on an abstract type T safe, 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

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]                                             ^^^^^^

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

case List(a, rest @ _*)

case List(a, rest*) (the Scala 2 form still compiles on 3.10)

Chained match and xs.match

Not available.

Available, because match binds like an infix operator.

Scrutinee with a type ascription

x: T match { ... } works.

Write (x: T) match ....

unapply result type

Must be Boolean or Option of the value.

Option-less extractors are allowed (Boolean, product, single and name-based matches). See Extractors & Pattern Bindings.

Strict equality and enums

strictEquality does not exist.

Opt-in strictEquality, with SIP-67 for matches (stable in 3.10).

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 match and TypeTest have 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 match on an enum or object under strictEquality may still need a CanEqual instance.

  • Sub-cases are experimental on both 3.9 and 3.10, behind the same subCases import.

  • The Matchable warning for a pattern selector of type Any is still -source:future only 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 @ _* and xs: _* splices. Prefer rest*; the expression form xs: _* 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 Any without discussing Matchable. It matters only under -source:future, but it explains why Any is not the right type for open-ended values.

  • x: T match ... and the lack of chained match. Scala 3 requires parentheses around an ascribed scrutinee.

  • Braces around the case clauses. They are optional.

  • No mention of strictEquality (Scala 3.0) or of SIP-67 and sub-cases (Scala 3.8 to 3.10).