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
Source: Scala 3 Book: Partial Functions
Notes on the example:
-
warningis 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 singlecaseon the same line as the=is not accepted on 3.10.0 without-preview; see SIP-74: parenthesised literals (preview in 3.10). -
fullyWrittenis what the literal means: a class withisDefinedAtandapply. The compiler generatesisDefinedAtfrom the patterns and guards, and anapplyOrElsethat evaluates the match only once. -
Calling
applyon an undefined value throwsscala.MatchError. Always test withisDefinedAt, or uselift,applyOrElseorcollect, which never throw. -
PartialFunction[A, B]extendsA => 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 |
|---|---|
|
Is the function defined at |
|
The value at |
|
The value at |
|
A total function returning |
|
Tries this function first, then |
|
Applies |
|
|
|
Runs |
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).
Source: Scala API: scala.PartialFunction
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
Source: Scala API: scala.PartialFunction
Points to note:
-
grade.isDefinedAt(-1)istruebecausegentleaccepts everyn < 300, negative numbers included; a union of partial functions is defined wherever any member is. -
orElsetries the functions left to right, so order matters when their domains overlap, exactly as with ordinarycaseclauses. -
steep.andThen(shout)stays partial:loud.isDefinedAt(10)isfalse. The result has the same domain assteep. -
composewith 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.unliftis how an extractor-likeA => Option[B]becomes a partial function usable withcollect.
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)
Source: Scala 3 Book: Partial Functions
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
Source: Scala 3 Book: Partial Functions
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 aPartialFunction[K, V], defined at its keys. -
Seq[A]is aPartialFunction[Int, A], defined at its valid indices. -
Set[A]is a totalA => 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,collectorrunWith.PartialFunction.fromFunctionandFunction.unliftare part of the 2.13 library as well. -
Java. There is no direct equivalent. A
Predicateplus aFunction, or aMap.getreturningOptional, play a similar role; pattern-matchingswitch(Java 21) is closed over its cases and has noisDefinedAt. -
Kotlin. No partial functions; code uses
mapNotNull, orwhenwith a default branch.mapNotNullcorresponds toflatMap(pf.lift)in Scala.
Pitfalls
-
MatchErrorfromapply. APartialFunctionis anA => B, so it can be passed tomap, which callsapplyfor every element.List(1, 2).map(warning)throws on the first undefined value. Usecollectorliftwhere the function is partial. -
Guards evaluated twice. Calling
isDefinedAt(x)and thenapply(x)runs the guards twice; preferapplyOrElse,lift, orrunWith, which evaluate once. This matters if a guard has a side effect or is expensive. -
orElseorder. Overlapping domains are resolved left to right. -
Silent filtering.
collectdrops elements without a warning. A typo in a pattern gives a shorter result rather than an error. -
Exhaustivity is not checked. For a
PartialFunctionthe 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
collecta method ofTraversableLike; it is now defined onIterableOps/IterableOnceOps. -
Few books cover
applyOrElseandrunWith, which are the efficient forms.