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 with isDefinedAt to check input validity before application
  • Pattern matching creates partial functions implicitly through case statements, enabling elegant composition with orElse, andThen, and applyOrElse combinators
  • Partial functions excel in collection operations like collect and 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.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.