Quoted 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). Every example was compiled and run with Scala 3.10.0 through Scala CLI 1.17.1 on JDK 26 (checked on 2026-10-15), with each macro in its own file and its use in another one (see Macros: Quotes & Splices for why). Quoted patterns are part of Scala 3 since 3.0. The explicit type variable syntax of SIP-53 is available since Scala 3.4.0, so it works on the 3.9 LTS and 3.10 but not on the 3.3 LTS. The examples ran on 3.10.0 only.

A macro that takes an Expr[T] often wants to know what the expression looks like: is it a literal, a sum, a call to a particular method, a lambda? The quotes.reflect API can answer that, but it means navigating compiler trees by hand. A quoted pattern does the same job in the notation of the language: you write the shape you expect as a quote and match on the Expr. The examples use the trailhead scenario, a toolkit that plans hikes on a network of mountain trails.

Quote patterns

A quote '{ ... } in pattern position is a pattern that matches an Expr. The scrutinee is an Expr[T] and the quote describes the code it must contain. Inside the quote, a splice $x is a binder: it matches any sub-expression and binds it as an Expr named x. A $x at the position of a literal matches any expression there.

A macro that simplifies integer arithmetic at compile time shows the whole idea.

//> using scala 3.10.0

package trailhead

import scala.quoted.*

inline def simplify(inline e: Int): Int = ${ simplifyImpl('e) }

def simplifyImpl(e: Expr[Int])(using Quotes): Expr[Int] =
  e match
    case '{ ($x: Int) + 0 }           => simplifyImpl(x)
    case '{ 0 + ($x: Int) }           => simplifyImpl(x)
    case '{ ($x: Int) * 1 }           => simplifyImpl(x)
    case '{ ($x: Int) * 0 }           => '{ 0 }
    case '{ ($x: Int) + ($y: Int) }   => '{ ${ simplifyImpl(x) } + ${ simplifyImpl(y) } }
    case '{ ($x: Int) * ($y: Int) }   => '{ ${ simplifyImpl(x) } * ${ simplifyImpl(y) } }
    case _                            => e

Each case is a quoted pattern. The ascription ($x: Int) gives the binder its type, and x is available on the right-hand side as an Expr[Int]. The right-hand side returns a new Expr, built with ordinary quotes. The recursion simplifies sub-expressions first. The last case returns the expression unchanged.

package trailhead

@main def run(): Unit =
  val n = 4
  println(simplify(n * 1 + 0))
  println(simplify((n + 0) * (1 * n)))
  println(simplify(n * 0 + n))
4
16
4

The printed values are the same as without the macro, so to see what the macro did, compile with -Vprint:inlining. The three calls became:

println(n:Int)
println((n * (1 * n)):Int)
println((0 + n):Int)

The first call lost both the * 1 and the + 0. The second lost only the + 0, because the patterns for * 1 only look at the right operand and the call has 1 * n. The third shows that n * 0 was replaced by the constant 0, and that 0 + n was not simplified again, because the pattern case + ($y: Int) simplifies each operand but does not re-run the rules on the result. A real optimiser would iterate until nothing changes.

What patterns can bind

A pattern in a quote is matched against the typed tree of the expression, after the compiler has done its usual desugaring. Several rules follow from that.

  • A splice $x binds an Expr. The extractor form ${ Expr(n) } goes further and binds the constant itself, using the FromExpr extractor (see Macros: Quotes & Splices). It only matches when the expression is a literal, as in the FromExpr[Trail] instance on that page.

  • An ascription ($x: T) fixes the type of the binder. Without it the type is inferred from the position.

  • A pattern matches the code as the compiler typed it, not as the user wrote it, so it is the same for one source form and its desugaring. The -> example below shows the consequence.

  • The binders are Exprs of the same Quotes as the scrutinee, so they can be spliced straight back into a new quote.

  • A case that does not match falls through to the next one, as in any match. A final case _ is almost always needed because Expr is not a sealed type.

A pitfall is that the patterns see the program after earlier compiler steps. The next macro tries to recognise a few shapes of an expression and print what it found:

inline def showShape[A](inline e: A): String = ${ shapeImpl('e) }

def shapeImpl[A: Type](e: Expr[A])(using Quotes): Expr[String] =
  e match
    case '{ $xs: List[t] }        => Expr(s"a List of ${Type.show[t]}")
    case '{ Some($v: t) }         => Expr(s"a Some of ${Type.show[t]}")
    case '{ ($a: Int) -> $b }     => Expr("an Int pair")
    case '{ if $c then $t else $f } => Expr("an if expression")
    case _                         => Expr("something else: " + e.show)
println(showShape(List(1, 2)))
println(showShape(Some("x")))
println(showShape(1 -> 2))
println(showShape(if n > 1 then 1 else 2))
println(showShape(n))
a List of scala.Int
a Some of java.lang.String
something else: (scala.Tuple2.apply[scala.Int, scala.Int](1, 2): scala.Tuple2[scala.Int, scala.Int])
an if expression
something else: n

The third call was not recognised as the arrow pattern. By the time the macro runs, the extension 1 -> 2 has already been inlined into the tuple constructor Tuple2.apply(1, 2), so the pattern ($a: Int) -> $b does not see an arrow. When a pattern does not match something that looks obvious, print e.show or e.asTerm.show(using Printer.TreeStructure) (see Reflection with quotes.reflect) to see the tree that the pattern is compared against.

Type variables in quoted patterns

In the cases above, $xs: List[t] and Some($v: t) use a lowercase name t that is not defined anywhere. In a quoted pattern, a lowercase type name that is not in scope is a type variable, exactly as in an ordinary pattern. It is bound to the matching type, is valid on the right-hand side of the case, and the pattern also provides the given Type[t] that quotes on the right-hand side need. These are the nested type variables.

The first case of shapeImpl printed scala.Int and the second printed java.lang.String, which are the types the variable was bound to at those call sites. The simple pattern case '{ $e: tpe } => is a useful special case: it binds tpe to the precise type of the expression, which can be narrower than the static type (the guide gives Some[Int] and Option[1] as examples), and '{ val x: tpe = $e; x } re-binds the value without widening it.

Nested variables have two limits: they cannot be referred to more than once, and they cannot have bounds. SIP-53 adds explicit type variables for those cases. They are declared at the start of the pattern with type, optionally with bounds, and are then referred to with backticks, as with stable identifiers in a normal pattern:

//> using scala 3.10.0

package trailhead

import scala.quoted.*

inline def fuseMap[T](inline xs: List[T]): List[T] = ${ fuseMapImpl('xs) }

def fuseMapImpl[T: Type](x: Expr[List[T]])(using Quotes): Expr[List[T]] =
  x match
    case '{ type u; type v; ($ls: List[`u`]).map($f: `u` => `v`).map($g: `v` => T) } =>
      '{ $ls.map(y => $g($f(y))) }
    case _ => x
package trailhead

@main def run(): Unit =
  println(fuseMap(List(1, 2, 3).map(x => x + 1).map(y => y.toString + "!")))
  println(fuseMap(List("a")))
List(2!, 3!, 4!)
List(a)

The pattern declares u and v once and refers to them with backticks, so f is an Expr[u => v] and g an Expr[v => T]. The first call has an Int to Int stage and an Int to String stage, and the generated code is one map: the -Vprint:inlining output shows a single map[String] call with the two bodies in sequence. Bounds are added on the declaration, type tail <: Tuple, as in the SIP page’s own examples. SIP-53 is marked as available in Scala 3.4.0.

Matching types

A type pattern '[T] matches a Type. Type.of[A] produces the type of the macro’s type parameter and the pattern says what is acceptable.

inline def isString[A]: Boolean = ${ isStringImpl[A] }

def isStringImpl[A: Type](using Quotes): Expr[Boolean] =
  Type.of[A] match
    case '[String]    => Expr(true)
    case '[List[t]]   => Expr(false)
    case '[Option[t]] => Expr(false)
    case _            => Expr(false)

inline def elementType[A](inline xs: A): String = ${ elementTypeImpl('xs) }

def elementTypeImpl[A: Type](xs: Expr[A])(using Quotes): Expr[String] =
  Type.of[A] match
    case '[List[t]]  => Expr(Type.show[t])
    case '[Array[t]] => Expr("array of " + Type.show[t])
    case '[t]        => Expr("not a collection: " + Type.show[t])
println(isString[String])
println(elementType(List(1.5)))
println(elementType(Array("a")))
println(elementType(3))
true
scala.Double
array of java.lang.String
not a collection: scala.Int

'[List[t]] matches any List and binds its element type to t. The pattern '[t] alone matches any type and is the catch-all that also names the type. Type patterns use subtyping: '[Seq[t]] would match a List[Int] too, and the cases are tried in order, so the specific cases go first. Because the Type is the static type at the call, elementType(xs) for a value with static type Any takes the last case even if the value is a list at runtime. This is a compile-time match, not an isInstanceOf.

Higher-order patterns (HOAS)

A pattern can also look inside a lambda or a val body. The body mentions the lambda’s parameter, a variable that is not in scope at the splice. A pattern cannot bind that body to an ordinary Expr because it is not a closed expression. Scala 3 handles this with higher-order abstract syntax: you write $f(x) applying the splice to the bound variable, and the binder f is the lambda body as a function of x.

//> using scala 3.10.0

package trailhead

import scala.quoted.*

inline def fuse(inline e: List[Int]): List[Int] = ${ fuseImpl('e) }

def fuseImpl(e: Expr[List[Int]])(using Quotes): Expr[List[Int]] =
  e match
    case '{ ($xs: List[Int]).map(x => ($f(x): Int)).map(y => ($g(y): Int)) } =>
      '{ $xs.map(z => ${ Expr.betaReduce('{ $g(${ Expr.betaReduce('{ $f(z) }) }) }) }) }
    case _ => e

inline def describeLambda(inline f: Int => Int): String = ${ describeImpl('f) }

def describeImpl(f: Expr[Int => Int])(using Quotes): Expr[String] =
  f match
    case '{ (x: Int) => ($body(x): Int) } =>
      Expr("lambda whose body at 0 is " + Expr.betaReduce('{ $body(0) }).show)
    case _ => Expr("not a literal lambda: " + f.show)
package trailhead

@main def run(): Unit =
  println(fuse(List(1, 2, 3).map(x => x + 1).map(y => y * 2)))
  println(describeLambda(x => x + 1))
  val f = (x: Int) => x * 2
  println(describeLambda(f))
List(4, 6, 8)
lambda whose body at 0 is 1
not a literal lambda: f

fuse recognises two consecutive map calls with literal lambdas and replaces them by a single map, which avoids the intermediate list. In the pattern, f and g stand for the two lambdas' bodies. In the right-hand side they are used as Exprs of function type applied with $f(z), and Expr.betaReduce rewrites the application of a lambda expression to an argument into the body with the argument substituted. Without betaReduce the generated code would contain ((x: Int) => x + 1).apply(z). The -Vprint:inlining output of the first call shows the fusion happened:

List.apply[Int]([1,2,3 : Int]*).map[Int]((z: Int) =>
  {
    val y: Int = z + 1
    y * 2
  }
)

The second macro shows two details of HOAS patterns. The splice $body(x) needs its type to be fully defined, and the type ascription is required. Without it the compiler reports:

-- Error: Hoas.scala:20:25
20 |    case '{ (x: Int) => $body(x) } => Expr(...)
   |                         ^^^^
   |                 Type must be fully defined.
   |                 Consider annotating the splice using a type ascription:
   |                   ($body(x): XYZ).

The second detail is that the binders of a lambda pattern are expressions of function type: in fuseImpl the compiler types f as Expr[Int => Int], so it is applied with $f(z) inside a quote. The output body at 0 is 1 is the body x + 1 evaluated for x = 0 by betaReduce, after constant folding. The third call, where the argument is the name of an existing function value, is not a literal lambda and falls to the default case.

Limits and guidelines

  • Quoted patterns match typed trees, so they are robust to formatting and to infix versus dotted syntax. They are also sensitive to compiler desugaring (inlined extensions, Tuple2.apply for ->, constant folding). Always test with the call shapes your users write.

  • A pattern cannot express "any tree of this kind"; for that, use quotes.reflect and match on Term subclasses. A good rule is to start with quoted patterns and drop to reflection only for what they cannot say.

  • The patterns of a match are not checked for exhaustiveness. Always add the case _ that returns the expression unchanged, so that the macro degrades to the original code.

  • Variables bound in a pattern are Exprs of the macro’s Quotes. They cannot escape the expansion (see Staging & TASTy Inspection for what happens when a Expr leaves its scope).

Scala 2 and Java comparison

Scala 2 quasiquotes, q"$a + 0", are used for both building and matching, but they work on untyped trees, so a pattern such as q"$x + 0" also matches a + method of a user-defined class. Scala 3 quoted patterns are typed: '{ ($x: Int) + 0 } only matches integer addition, and x is an Expr[Int]. Java has no counterpart. The nearest are the pattern matching on sealed interfaces and record patterns, which work on runtime values, not on program text. See Pattern Matching for the runtime side.

Scala 3.9 LTS versus 3.10

  • Quoted patterns, nested type variables and Expr.betaReduce have been part of Scala 3 since 3.0. The explicit type variable syntax of SIP-53 is available since 3.4.0, so the 3.9 LTS and 3.10 have it and the 3.3 LTS does not.

  • The examples were run on 3.10.0 only.

Where the books are outdated

  • Books and blog posts written before Scala 3.4 do not have the explicit type t <: Bound declaration with backticked references of SIP-53. Only the nested lowercase type variables existed.

  • Scala 2 material uses quasiquotes and c.universe trees, which do not exist in Scala 3.

  • Older macro tutorials match on Expr with quotes.reflect trees in all cases. A quoted pattern is shorter and checked, so prefer it when it can express the shape.