Scala Partial Functions
A partial function is a function that is only defined for some inputs, not all. Unlike a total function (defined for every possible input), a partial function can say "I don't handle this input." Scala represents partial functions with the PartialFunction[A, B] type and the case literal syntax.
Total vs Partial Functions
Total function: Partial function:
──────────────────── ─────────────────────────────────
def f(x: Int): Int = val f: PartialFunction[Int, Int] = {
x * 2 case x if x > 0 => x * 2
}
Works for ALL Int Only works for positive Int
f(-5) = -10 f(-5) = MatchError (not defined)
Defining a Partial Function
val doublePositive: PartialFunction[Int, Int] = {
case n if n > 0 => n * 2
}
println(doublePositive(5)) // 10
println(doublePositive(10)) // 20
// doublePositive(-3) // throws MatchError — not defined!
isDefinedAt — Checking Before Calling
val describeNumber: PartialFunction[Int, String] = {
case 0 => "zero"
case n if n > 0 => "positive"
}
println(describeNumber.isDefinedAt(5)) // true
println(describeNumber.isDefinedAt(0)) // true
println(describeNumber.isDefinedAt(-7)) // false
// Safe call using isDefinedAt
val input = -7
if describeNumber.isDefinedAt(input) then
println(describeNumber(input))
else
println(s"$input is not handled")
// -7 is not handled
collect — Apply Only Where Defined
collect applies a partial function to a collection, keeping only the elements where the function is defined:
val mixed: List[Any] = List(1, "hello", 2, true, "world", 3, false)
val onlyInts: List[Int] = mixed.collect { case n: Int => n }
println(onlyInts) // List(1, 2, 3)
val onlyStrings = mixed.collect { case s: String => s.toUpperCase }
println(onlyStrings) // List(HELLO, WORLD)
val parse: PartialFunction[String, Int] = {
case s if s.matches("\\d+") => s.toInt
}
val tokens = List("42", "abc", "100", "xyz", "7")
val numbers = tokens.collect(parse)
println(numbers) // List(42, 100, 7)
tokens: ["42", "abc", "100", "xyz", "7"]
parse defined at: "42" ✓ "abc" ✗ "100" ✓ "xyz" ✗ "7" ✓
result: [42, 100, 7]
Combining Partial Functions with orElse
val handlePositive: PartialFunction[Int, String] = {
case n if n > 0 => s"Positive: $n"
}
val handleNegative: PartialFunction[Int, String] = {
case n if n < 0 => s"Negative: $n"
}
val handleZero: PartialFunction[Int, String] = {
case 0 => "Zero"
}
val handleAll = handlePositive orElse handleNegative orElse handleZero
List(-3, 0, 5, -1, 9).map(handleAll).foreach(println)
// Negative: -3
// Zero
// Positive: 5
// Negative: -1
// Positive: 9
andThen — Chaining Partial Functions
val parseNumber: PartialFunction[String, Int] = {
case s if s.matches("-?\\d+") => s.toInt
}
val doubleIt: PartialFunction[Int, String] = {
case n => s"doubled: ${n * 2}"
}
val pipeline = parseNumber andThen doubleIt
println(pipeline("42")) // doubled: 84
println(pipeline("-10")) // doubled: -20
println(pipeline.isDefinedAt("abc")) // false
Partial Functions in match
Every match expression with cases is actually a partial function in Scala:
// This is a PartialFunction[Any, String]
val describe: PartialFunction[Any, String] = {
case n: Int if n > 0 => s"positive int $n"
case n: Int if n <= 0 => s"non-positive int $n"
case s: String => s"string: '$s'"
case _: Boolean => "a boolean"
}
println(describe(42)) // positive int 42
println(describe(-3)) // non-positive int -3
println(describe("hello")) // string: 'hello'
println(describe(true)) // a boolean
Practical: Event Handler
sealed trait Event
case class Click(x: Int, y: Int) extends Event
case class KeyPress(key: Char) extends Event
case class Scroll(delta: Int) extends Event
case class Resize(w: Int, h: Int) extends Event
val handleClick: PartialFunction[Event, String] = {
case Click(x, y) => s"Clicked at ($x, $y)"
}
val handleKey: PartialFunction[Event, String] = {
case KeyPress(k) if k.isLetter => s"Letter pressed: $k"
case KeyPress(k) => s"Non-letter key: $k"
}
val handleScroll: PartialFunction[Event, String] = {
case Scroll(d) if d > 0 => "Scrolled down"
case Scroll(_) => "Scrolled up"
}
val handler = handleClick orElse handleKey orElse handleScroll
val events: List[Event] = List(
Click(100, 200),
KeyPress('A'),
Scroll(-3),
KeyPress('1'),
Click(50, 75)
)
events
.filter(handler.isDefinedAt)
.map(handler)
.foreach(println)
// Clicked at (100, 200)
// Letter pressed: A
// Scrolled up
// Non-letter key: 1
// Clicked at (50, 75)
