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)

Leave a Comment

Your email address will not be published. Required fields are marked *