1. Introduction

In this tutorial, we will be looking at partial functions in Scala. A partial function is a function applicable to a subset of the data it has been defined for.

For example, we could define a function on the Int domain that only works on odd numbers.

2. Understanding the Definition

If we look at the Scaladoc definition, there are three main characteristics to highlight.

A partial function:

  • is a unary operation, which means it only takes one parameter
  • is applicable to a subdomain of values
  • can explicitly include an isDefinedAt method, to define its domain, and an apply method

Let’s have a look at an example of a partial function:

val squareRoot: PartialFunction[Double, Double] = {
  def apply(x: Double) = Math.sqrt(x)
  def isDefinedAt(x: Double) = x >= 0
}

Analyzing the above function, we can see that it:

  • takes a single parameter, a Double
  • applies to a Double subdomain, such as x >= 0
  • has an isDefinedAt method and an apply method

Therefore, it is indeed a partial function.

isDefinedAt and apply are often implicitly defined. We can rewrite our function using a case statement. It’s a really common practice when writing partial functions:

val squareRootImplicit: PartialFunction[Double, Double] = {
  case x if x >= 0 => Math.sqrt(x)
}

Invoking our partial function on a negative number will return a scala.MatchError runtime error.

3. Chaining With orElse and andThen

Chaining is a very useful feature of partial functions.

Let’s say we want to convert positive integers to negative and vice-versa. Clearly, just multiplying by the integer -1 would do the trick.

However, to demonstrate the chaining feature, our solution will include:

  • If the number is negative or zero, then return its absolute value
  • If the number is positive, then return the number multiplied by -1

Therefore, we can define a partial function for each requirement:

val negativeOrZeroToPositive: PartialFunction[Int, Int] = {
  case x if x <= 0 => Math.abs(x)
}

val positiveToNegative: PartialFunction[Int, Int] = {
  case x if x > 0 => -1 * x
}

Then we can leverage the chaining operator orElse to use them together:

val swapSign: PartialFunction[Int, Int] = {
  positiveToNegative orElse negativeOrZeroToPositive
}

Additionally, the PartialFunction trait also includes andThen. As a result, chaining is really easy to achieve.

To show how andThen works, let’s begin with a function that only prints positive Int values:

val printIfPositive: PartialFunction[Int, Unit] = {
  case x if x > 0 => println(s"$x is positive!")
}

We can now chain this with our swapSign function:

(swapSign andThen printIfPositive)(-1)

The final result is easy-to-chain code that reads quite nicely.

4. Working With Collections

When working with collections, there are several methods that are well-suited for partial functions.

Let’s have a look at some of the main ones, behaving in a not-so-obvious way.

4.1. collect, map, and filter

collect is a method that, given a collection, will return a new collection by applying a partial function to the elements in its domain.

Let’s define a partial function to be used by the collect method:

val parseRange: PartialFunction[Int, Int] = {
  case x: Int if x > 10 => x + 1
}
List(15, 3, "aString") collect { parseRange }

Our partial function will only be applied to 15 since it is the only Int greater than 10 in our List.

Therefore, the above will return List(16).

We can use parseRange with map as well, without creating any compilation error:

List(15, 3, "aString") map { parseRange }

This code will throw a scala.MatchError at runtime because the pattern match doesn’t know how to handle a String.

The last method we’ll look at is filter, which is similar to collect, except filter returns a collection of elements that satisfy a provided condition.

We can see the different results returned using the same partial function to show that the collect and filter methods behave differently:

List(1, 2) collect { case i: Int => i > 10 }
List(1, 2) filter { case i: Int => i > 10 }

In this case, collect returns List(false, false), while filter returns an empty list. Also, notice how we defined the partial function as an anonymous function.

5. Conclusion

In this article, we looked at partial functions. We showed that chaining partial functions and using them with collection methods is pretty straightforward.

Whenever we’re aiming for highly composable code, partial functions are definitely something to keep in mind. When used with collections in particular, methods like collect, map, and filter make data manipulation and transformation easy.

The combination of these characteristics makes partial functions a powerful tool.

As always, the code is available over on GitHub.

guest
0 Comments
Inline Feedbacks
View all comments