Partial Functions

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 PartialFunction trait and the case-clause literals have not changed since Scala 2, so they work the same on the 3.9 LTS (3.9.0, 2026-09-03) and the 3.3 LTS. The one new piece, the parenthesised literal of SIP-74, is a preview feature in 3.10.0 and is described in SIP-74: parenthesised literals (preview in 3.10).

A partial function is a function that is defined for only some of the values of its parameter type. Asking for the logarithm of a negative number or the element of a list at an index that does not exist are everyday examples. Scala represents the idea with the trait PartialFunction[-A, +B], a subtype of A => B that adds a test, isDefinedAt, telling whether the function accepts a value. The examples use the trailhead scenario, a toolkit that plans hikes on a network of mountain trails. For the patterns used in the case clauses see Pattern Matching; for ordinary function values see Functions as Values.

Defining a partial function

A block of case clauses with no scrutinee is a partial function literal when the expected type is PartialFunction. Each clause is a pattern, an optional guard and a body; the function is defined exactly where some clause matches.

//> using scala 3.10.0

enum Difficulty:
  case Easy, Moderate, Hard

val warning: PartialFunction[Int, String] =
  case n if n > 1000 => s"steep: $n m of ascent"

val fullyWritten = new PartialFunction[Int, String]:
  def isDefinedAt(n: Int): Boolean = n > 1000
  def apply(n: Int): String = s"steep: $n m of ascent"

@main def run(): Unit =
  println(warning.isDefinedAt(1200))
  println(warning.isDefinedAt(300))
  println(warning(1200))
  println(fullyWritten(1500))
  println(warning.applyOrElse(300, n => s"ok: $n m"))
  println(warning.lift(1200))
  println(warning.lift(300))
  val ascents = List(300, 1200, 800, 1500)
  println(ascents.collect(warning))
  println(ascents.collectFirst(warning))
  println(ascents.map(warning.lift))
  println(ascents.flatMap(warning.lift))
  try warning(300)
  catch case e: MatchError => println(s"MatchError: ${e.getMessage}")
  println(warning.isInstanceOf[Function1[?, ?]])
  val asFunction: Int => String = warning
  println(asFunction(2000))
true
false
steep: 1200 m of ascent
steep: 1500 m of ascent
ok: 300 m
Some(steep: 1200 m of ascent)
None
List(steep: 1200 m of ascent, steep: 1500 m of ascent)
Some(steep: 1200 m of ascent)
List(None, Some(steep: 1200 m of ascent), None, Some(steep: 1500 m of ascent))
List(steep: 1200 m of ascent, steep: 1500 m of ascent)
MatchError: 300 (of class java.lang.Integer)
true
steep: 2000 m of ascent

Notes on the example:

  • warning is written with the cases on the line after the =. In Scala 3 the cases may follow a colon or an = at the end of a line, or sit inside braces (the Scala 2 form { case n if n > 1000 => ... }). Writing the single case on the same line as the = is not accepted on 3.10.0 without -preview; see SIP-74: parenthesised literals (preview in 3.10).

  • fullyWritten is what the literal means: a class with isDefinedAt and apply. The compiler generates isDefinedAt from the patterns and guards, and an applyOrElse that evaluates the match only once.

  • Calling apply on an undefined value throws scala.MatchError. Always test with isDefinedAt, or use lift, applyOrElse or collect, which never throw.

  • PartialFunction[A, B] extends A => B, so a partial function can be given wherever a total function is expected. The type checker will not warn when it is later applied to an undefined value; see Pitfalls.

The members of PartialFunction

Member Meaning

def isDefinedAt(x: A): Boolean

Is the function defined at x?

def apply(x: A): B

The value at x, or throws MatchError.

def applyOrElse[A1 <: A, B1 >: B](x: A1, default: A1 => B1): B1

The value at x if defined, else default(x). Evaluates the guards once.

def lift: A => Option[B]

A total function returning Some where defined and None elsewhere.

def orElse[A1 <: A, B1 >: B](that: PartialFunction[A1, B1])

Tries this function first, then that. Defined where either is.

def andThen[C](k: B => C): PartialFunction[A, C]

Applies k to the result. Defined where this one is. If k is itself a PartialFunction, the result is defined only where both are.

def compose[R](k: R => A): R => B

k first, then this function. If k is a PartialFunction, the result is a PartialFunction.

def runWith[U](action: B => U): A => Boolean

Runs action on the result when defined and returns whether it was defined.

The companion object adds PartialFunction.cond, PartialFunction.condOpt, PartialFunction.empty and PartialFunction.fromFunction, and Function.unlift turns an A => Option[B] into a partial function (the inverse of lift).

Combining partial functions

orElse builds a larger partial function from smaller ones, which is the standard way to compose handlers. andThen and compose add a step after or before. lift and unlift convert between partial functions and Option-returning functions.

//> using scala 3.10.0

val steep: PartialFunction[Int, String] =
  case n if n > 1000 => "steep"

val gentle: PartialFunction[Int, String] =
  case n if n < 300 => "gentle"

val moderate: PartialFunction[Int, String] =
  case n if n >= 300 && n <= 1000 => "moderate"

val grade = steep.orElse(gentle).orElse(moderate)

val shout: String => String = _.toUpperCase
val loud = steep.andThen(shout)

val toLength: PartialFunction[String, Int] =
  case "Pico" => 1200

val chained = steep.compose(toLength)

val unlifted = Function.unlift((s: String) => if s.startsWith("P") then Some(s.length) else None)

@main def run(): Unit =
  println(List(100, 500, 1500).map(grade))
  println(grade.isDefinedAt(-1))
  println(steep.orElse(gentle).isDefinedAt(500))
  println(loud(1200))
  println(loud.isDefinedAt(10))
  println(chained.isDefinedAt("Pico"))
  println(chained("Pico"))
  println(chained.isDefinedAt("Lago"))
  println(unlifted.isDefinedAt("Pico"))
  println(List("Pico", "Lago", "Peak").collect(unlifted))
  val pf1 = steep.andThen[Int](_.length)
  println(pf1(1500))
  val lifted: Int => Option[String] = steep.lift
  println(List(1200, 10).map(lifted))
  val andThenPf = steep.andThen(PartialFunction.fromFunction((s: String) => s.length))
  println(andThenPf(1200))
  println(steep.runWith(s => println(s"got $s"))(1200))
  println(steep.runWith(s => println(s"got $s"))(5))
List(gentle, moderate, steep)
true
false
STEEP
false
true
steep
false
true
List(4, 4)
5
List(Some(steep), None)
5
got steep
true
false

Points to note:

  • grade.isDefinedAt(-1) is true because gentle accepts every n < 300, negative numbers included; a union of partial functions is defined wherever any member is.

  • orElse tries the functions left to right, so order matters when their domains overlap, exactly as with ordinary case clauses.

  • steep.andThen(shout) stays partial: loud.isDefinedAt(10) is false. The result has the same domain as steep.

  • compose with a partial function produces a partial function defined where the inner one is defined and its result is in the domain of the outer one.

  • Function.unlift is how an extractor-like A => Option[B] becomes a partial function usable with collect.

collect, collectFirst and friends

The most common use of a partial function is collect, which is filter and map in one step: it keeps only the elements for which the function is defined and applies it to them. Because the literal can be written inline as a block of cases, this is also the idiomatic way to deconstruct elements with patterns.

//> using scala 3.10.0

case class Trail(name: String, lengthKm: Double, ascentM: Int)

@main def run(): Unit =
  val trails = List(Trail("Pico", 12.5, 900), Trail("Lago", 4.0, 150), Trail("Cumbre", 8.5, 1200))
  // map with a total function: must handle every trail
  val names = trails.map:
    case Trail(n, _, _) => n
  println(names)
  // collect: filter and map in one step
  val steepNames = trails.collect:
    case Trail(n, _, a) if a > 500 => n
  println(steepNames)
  // the same with filter + map
  println(trails.filter(_.ascentM > 500).map(_.name))
  // collect with a type pattern
  val mixed = List[Any](1, "two", 3.0, "four")
  println(mixed.collect { case s: String => s.length })
  // foreach with cases
  trails.foreach:
    case Trail(n, km, _) if km > 10 => println(s"$n is long")
    case Trail(n, _, _)             => println(s"$n is short")
  // a block of cases where a Function1 is expected
  val describe: Trail => String =
    case Trail(n, _, a) if a > 1000 => s"$n: alpine"
    case Trail(n, _, _)             => s"$n: ordinary"
  println(trails.map(describe))
List(Pico, Lago, Cumbre)
List(Pico, Cumbre)
List(Pico, Cumbre)
List(3, 4)
Pico is long
Lago is short
Cumbre is short
List(Pico: ordinary, Lago: ordinary, Cumbre: alpine)

The same literal syntax produces a total function when the expected type is A => B (as for map, foreach and describe), and a partial function when the expected type is PartialFunction (as for collect). When the expected type is a function, the compiler treats the cases as x => x match { ... }, so a missing case is a MatchError at runtime, and for a sealed type the compiler reports non-exhaustive matches as a warning:

//> using scala 3.10.0
sealed trait Leg
case class Climb(m: Int) extends Leg
case class Descent(m: Int) extends Leg
@main def run(): Unit =
  val f: Leg => Int = {
    case Climb(m) => m
  }
  val pf: PartialFunction[Leg, Int] = {
    case Climb(m) => m
  }
  println(pf.isDefinedAt(Descent(3)))
  println(f(Climb(2)))
-- [E029] Pattern Match Exhaustivity Warning: exh.scala:7:4
7 |    case Climb(m) => m
  |    ^
  |    match may not be exhaustive.
  |
  |    It would fail on pattern case: Descent(_)
  |
  | longer explanation available when compiling with `-explain`
1 warning found
false
2

Only the total function f is warned about (line 7); the partial function pf is allowed to leave cases out, because that is its point. Other collection methods that take a partial function are collectFirst (the first defined result, as an Option) and the collect methods of Iterator, Option, Future and Map. The collection methods are covered in Collection Operations.

Maps, sequences and sets are partial functions

Several standard types extend PartialFunction, so they can be passed wherever one is wanted:

  • Map[K, V] is a PartialFunction[K, V], defined at its keys.

  • Seq[A] is a PartialFunction[Int, A], defined at its valid indices.

  • Set[A] is a total A => Boolean, not a partial function.

//> using scala 3.10.0

@main def run(): Unit =
  val heights = Map("Pico" -> 2351, "Lago" -> 1100)
  val pfMap: PartialFunction[String, Int] = heights
  println(pfMap.isDefinedAt("Pico"))
  println(pfMap.isDefinedAt("Cumbre"))
  println(List("Pico", "Cumbre", "Lago").collect(heights))
  val seq = Seq("a", "b", "c")
  println(seq.isDefinedAt(2))
  println(seq.isDefinedAt(3))
  println(List(1, 5).collect(Seq("x", "y", "z")))
  val set = Set(1, 2)
  println(set(2))
  println(PartialFunction.cond("Pico") { case "Pico" => true })
  println(PartialFunction.cond("Lago") { case "Pico" => true })
  println(PartialFunction.condOpt(1200) { case n if n > 1000 => n * 2 })
  println(PartialFunction.condOpt(5) { case n if n > 1000 => n * 2 })
  val inc = PartialFunction.fromFunction((n: Int) => n + 1)
  println(inc.isDefinedAt(5))
  val empty = PartialFunction.empty[Int, Int]
  println(empty.isDefinedAt(5))
  val handler = heights.orElse(Map("Cumbre" -> 2000))
  println(handler("Cumbre"))
  println(heights.applyOrElse("Cumbre", _ => -1))
  println(heights.getOrElse("Cumbre", -1))
true
false
List(2351, 1100)
true
false
List(y)
true
true
false
Some(2400)
None
true
false
2000
-1
-1

PartialFunction.cond(x)(pf) is a one-line "does this value match the pattern?" test: it returns pf(x) when defined and false otherwise. PartialFunction.condOpt(x)(pf) returns Some(pf(x)) or None. PartialFunction.empty is defined nowhere, and PartialFunction.fromFunction wraps a total function as a partial one that is defined everywhere. Set is not a partial function: set(2) is the membership test and never throws.

SIP-74: parenthesised literals (preview in 3.10)

In Scala 3.9 and earlier a single-case literal has to be a block: either braces, or : / = with the cases on the next lines. SIP-74 (Partial function literals with parentheses) lets a case be written directly where an expression is expected, for instance as the only argument in a call or as the right-hand side of a val. According to the SIP page it is implemented and is available as a preview feature in Scala 3.10.0, and "will be activated by default in the next minor release", so it is not standard in 3.10.0, and it is not available on the 3.9 LTS or the 3.3 LTS. Enable it with the -preview compiler flag (//> using option -preview in a Scala CLI file; add -preview to scalacOptions in sbt). These examples were run on 3.10.0 with the flag.

//> using scala 3.10.0
//> using option -preview

case class Trail(name: String, lengthKm: Double, ascentM: Int)

@main def run(): Unit =
  val trails = List(Trail("Pico", 12.5, 900), Trail("Lago", 4.0, 150), Trail("Cumbre", 8.5, 1200))
  val steep = trails.collect(case Trail(n, _, a) if a > 500 => n)
  println(steep)
  val pf: PartialFunction[Int, String] = case n if n > 1000 => "steep"
  println(pf.isDefinedAt(5))
  val lifted = List(1200, 5).map(pf.lift)
  println(lifted)
  // map expects a total function: a literal with cases becomes a Function1 and a gap is a MatchError
  try println(List(1, 2, 3).map(case n if n % 2 == 1 => n * 10))
  catch case e: MatchError => println(s"MatchError: ${e.getMessage}")
List(Pico, Cumbre)
false
List(Some(steep), None)
MatchError: 2 (of class java.lang.Integer)

Without the flag the same code is rejected on 3.10.0, and the message does not mention it:

//> using scala 3.10.0
case class Trail(name: String, ascentM: Int)
@main def run(): Unit =
  val trails = List(Trail("Pico", 900), Trail("Lago", 150))
  println(trails.collect(case Trail(n, a) if a > 500 => n))
-- [E018] Syntax Error: nopreview.scala:5:25
5 |  println(trails.collect(case Trail(n, a) if a > 500 => n))
  |                         ^^^^
  |                         expression expected but case found
  |
  | longer explanation available when compiling with `-explain`
1 error found
Compilation failed

The parenthesised form takes one case clause. Several clauses inside the parentheses are not supported; with -preview on 3.10.0, collect(case 1 => "a" case 2 => "b") fails with "')' expected, but 'case' found". Use the block form for two or more clauses. As with the block form, the expected type decides what the literal is: a PartialFunction for collect, a total function for map. Because it is a preview feature, its details may still change; avoid it in code that must also compile on the LTS lines, and see Optional Braces & Indentation for the related relaxed lambda syntax (SIP-75), which is switched on by the same flag, and Getting Started & Versions for the preview status.

Scala 2 and other languages

  • Scala 2. The literal is always a brace block, { case x if x > 0 => ... }. That form is still valid in Scala 3, and there is no difference in the trait, lift, orElse, collect or runWith. PartialFunction.fromFunction and Function.unlift are part of the 2.13 library as well.

  • Java. There is no direct equivalent. A Predicate plus a Function, or a Map.get returning Optional, play a similar role; pattern-matching switch (Java 21) is closed over its cases and has no isDefinedAt.

  • Kotlin. No partial functions; code uses mapNotNull, or when with a default branch. mapNotNull corresponds to flatMap(pf.lift) in Scala.

Pitfalls

  • MatchError from apply. A PartialFunction is an A => B, so it can be passed to map, which calls apply for every element. List(1, 2).map(warning) throws on the first undefined value. Use collect or lift where the function is partial.

  • Guards evaluated twice. Calling isDefinedAt(x) and then apply(x) runs the guards twice; prefer applyOrElse, lift, or runWith, which evaluate once. This matters if a guard has a side effect or is expensive.

  • orElse order. Overlapping domains are resolved left to right.

  • Silent filtering. collect drops elements without a warning. A typo in a pattern gives a shorter result rather than an error.

  • Exhaustivity is not checked. For a PartialFunction the compiler does not warn about unmatched cases. A total function written with cases over a sealed type is checked, as the example above shows.

  • val pf: PartialFunction[A, B] = case ... on one line is a syntax error on 3.10.0 without -preview.

Where the books are outdated

  • Scala 2 books write every literal as { case ... }; Scala 3 accepts cases after a colon or = with optional braces, and SIP-74 adds a parenthesised single-case form as a preview in 3.10.0.

  • Books written before 2.13 call collect a method of TraversableLike; it is now defined on IterableOps/IterableOnceOps.

  • Few books cover applyOrElse and runWith, which are the efficient forms.