Scala - Partial Functions
A partial function in Scala is a function that is not defined for all possible input values of its domain. Unlike total functions that must handle every input, partial functions explicitly declare...
Key Insights
- Partial functions are defined only for a subset of possible inputs, using
PartialFunction[A, B]trait withisDefinedAtto check input validity before application - Pattern matching creates partial functions implicitly through
casestatements, enabling elegant composition withorElse,andThen, andapplyOrElsecombinators - Partial functions excel in collection operations like
collectand actor message handling, providing type-safe alternatives to exception-throwing code paths
Understanding Partial Functions
A partial function in Scala is a function that is not defined for all possible input values of its domain. Unlike total functions that must handle every input, partial functions explicitly declare which inputs they accept through the isDefinedAt method.
val divide: PartialFunction[(Int, Int), Int] = {
case (numerator, denominator) if denominator != 0 =>
numerator / denominator
}
println(divide.isDefinedAt((10, 2))) // true
println(divide.isDefinedAt((10, 0))) // false
println(divide((10, 2))) // 5
// divide((10, 0)) // MatchError
The PartialFunction[A, B] trait extends Function1[A, B] and adds the isDefinedAt(x: A): Boolean method. This allows runtime checking before applying the function, preventing exceptions.
Creating Partial Functions
Pattern matching is the most common way to create partial functions. Each case clause defines part of the function’s domain:
val numberClassifier: PartialFunction[Int, String] = {
case x if x < 0 => "negative"
case 0 => "zero"
case x if x > 0 => "positive"
}
val evenHandler: PartialFunction[Int, String] = {
case x if x % 2 == 0 => s"$x is even"
}
println(evenHandler.isDefinedAt(4)) // true
println(evenHandler.isDefinedAt(3)) // false
You can also construct partial functions explicitly:
val squareRoot = new PartialFunction[Double, Double] {
def isDefinedAt(x: Double): Boolean = x >= 0
def apply(x: Double): Double = Math.sqrt(x)
}
println(squareRoot.isDefinedAt(16.0)) // true
println(squareRoot.isDefinedAt(-4.0)) // false
println(squareRoot(16.0)) // 4.0
Composing Partial Functions
Partial functions support powerful composition operators that enable building complex logic from simple pieces.
Using orElse
The orElse method chains partial functions, trying each in sequence until one is defined:
val handlePositive: PartialFunction[Int, String] = {
case x if x > 0 => s"Positive: $x"
}
val handleNegative: PartialFunction[Int, String] = {
case x if x < 0 => s"Negative: $x"
}
val handleZero: PartialFunction[Int, String] = {
case 0 => "Zero"
}
val handleAll = handlePositive orElse handleNegative orElse handleZero
println(handleAll(5)) // Positive: 5
println(handleAll(-3)) // Negative: -3
println(handleAll(0)) // Zero
Using andThen
The andThen method composes partial functions sequentially, applying transformations in order:
val extractDigit: PartialFunction[String, Int] = {
case s if s.matches("\\d+") => s.toInt
}
val doubleIt: PartialFunction[Int, Int] = {
case x => x * 2
}
val processDigitString = extractDigit andThen doubleIt
println(processDigitString.isDefinedAt("42")) // true
println(processDigitString.isDefinedAt("abc")) // false
println(processDigitString("42")) // 84
Using applyOrElse
The applyOrElse method provides a fallback value when the function is not defined, avoiding exceptions:
val safeDivide: PartialFunction[(Int, Int), Double] = {
case (n, d) if d != 0 => n.toDouble / d
}
val result1 = safeDivide.applyOrElse((10, 2), (_: (Int, Int)) => Double.NaN)
val result2 = safeDivide.applyOrElse((10, 0), (_: (Int, Int)) => Double.NaN)
println(result1) // 5.0
println(result2) // NaN
Practical Applications
Collection Processing with collect
The collect method on collections uses partial functions to filter and transform elements simultaneously:
val mixed: List[Any] = List(1, "two", 3, "four", 5, 6.0)
val integers = mixed.collect {
case i: Int => i
}
val doubled = mixed.collect {
case i: Int => i * 2
}
println(integers) // List(1, 3, 5)
println(doubled) // List(2, 6, 10)
This is more elegant than combining filter and map:
case class Person(name: String, age: Int)
case class Employee(name: String, id: Int)
val people: List[Any] = List(
Person("Alice", 30),
Employee("Bob", 101),
Person("Charlie", 25),
"Invalid"
)
val employeeIds = people.collect {
case Employee(_, id) => id
}
println(employeeIds) // List(101)
State Machine Implementation
Partial functions naturally model state transitions:
sealed trait State
case object Idle extends State
case object Running extends State
case object Paused extends State
sealed trait Event
case object Start extends Event
case object Pause extends Event
case object Resume extends Event
case object Stop extends Event
type Transition = PartialFunction[(State, Event), State]
val transitions: Transition = {
case (Idle, Start) => Running
case (Running, Pause) => Paused
case (Paused, Resume) => Running
case (Running, Stop) => Idle
case (Paused, Stop) => Idle
}
def nextState(current: State, event: Event): Option[State] = {
transitions.lift((current, event))
}
println(nextState(Idle, Start)) // Some(Running)
println(nextState(Running, Pause)) // Some(Paused)
println(nextState(Idle, Pause)) // None (invalid transition)
Error Handling Pattern
Partial functions provide type-safe error handling without exceptions:
sealed trait Result[+A]
case class Success[A](value: A) extends Result[A]
case class Failure(error: String) extends Result[Nothing]
def parseConfig(input: String): Result[Map[String, String]] = {
val parser: PartialFunction[String, Map[String, String]] = {
case s if s.contains("=") =>
s.split("\n")
.map(_.split("="))
.collect { case Array(k, v) => k.trim -> v.trim }
.toMap
}
parser.lift(input)
.map(Success(_))
.getOrElse(Failure("Invalid configuration format"))
}
val config = "host=localhost\nport=8080"
println(parseConfig(config)) // Success(Map(host -> localhost, port -> 8080))
val invalid = "invalid config"
println(parseConfig(invalid)) // Failure(Invalid configuration format)
Advanced Patterns
Combining Multiple Partial Functions
Build complex validators from simple rules:
type Validator[A] = PartialFunction[A, List[String]]
val validateAge: Validator[Int] = {
case age if age < 0 => List("Age cannot be negative")
case age if age > 150 => List("Age seems unrealistic")
}
val validatePositive: Validator[Int] = {
case n if n <= 0 => List("Must be positive")
}
def validate[A](value: A, validators: Validator[A]*): List[String] = {
validators.flatMap(_.lift(value).getOrElse(Nil)).toList
}
println(validate(-5, validateAge, validatePositive))
// List(Age cannot be negative, Must be positive)
println(validate(30, validateAge, validatePositive))
// List()
Pattern Matching on Types
Extract and transform different types safely:
def processValue: PartialFunction[Any, String] = {
case s: String => s.toUpperCase
case i: Int => s"Number: $i"
case l: List[_] => s"List of ${l.size} items"
case Some(x) => s"Optional value: $x"
}
val values = List("hello", 42, List(1, 2, 3), Some("test"), None)
val processed = values.collect(processValue)
println(processed)
// List(HELLO, Number: 42, List of 3 items, Optional value: test)
Partial functions provide a powerful abstraction for handling subsets of input domains. They enable cleaner code through composition, safer error handling through explicit domain checking, and more expressive collection operations. Use them when you need to process only certain inputs, model state transitions, or build composable validation logic.