Partial functions in Scala

Versions: Scala 2.12.1

Recently in this series about Scala features I covered the partially applied functions. At first glance we could think they are the same as partial functions. But it's not really true and this post will explain why.

This post is composed of 3 sections. The first one defines the partial functions. The latter one shows how they can be implemented in Scala while the last one contains some learning tests with them.

Partial functions definition

To define partial functions it's good to compare them to the total functions. The latter ones are the functions we usually write but to highlight the difference in the context of this post's, topic they're called "total". Thus, these total functions are normal functions taking any number of arguments, making some operations on them and returning the result. Partial functions do the same except the fact that they aren't applied on all the parameters.

Let's take a very illustrative example of division. As you know, the division by 0 in Scala leads to the runtime ArithmeticException: / by zero. Since a partial function accepts only a subset of possible values, it's a good candidate to become the basis for a safe divide method because it can be written to apply only to the values different than 0.

More formally speaking, partial function is an unary function that provides an answer to only a subset of values. Thus, it restricts the data it can handle.

Regarding to the partially applied functions in Scala, partial functions are different. The first ones rely on the reduction of the number of function's parameters by fixing some of them. The partial functions are based on the principle of reducing the number of parameters accepted by the function.

Partial functions implementation details

In Scala the partial function is represented as an implementation of scala.PartialFunction[-A, +B] trait. Among its most important methods we can distinguish isDefinedAt(x: A). It's the method checking if the partial function is able to accept the parameter x.

Another important property of PartialFunction implementations is the ability to chain the calls. The orElse(that: PartialFunction[A1, B1]) method defines the partial function's behavior for the case when the parameters aren't accepted. Another chaining method, andThen[C](k: B => C) helps to specify the function handling the result computed by the partial function.

The partial function exposes also a possibility to be transformed to the total function. This operation is called lifting (unlifting does the inverse) and can be defined with lift: A => Option[B] method.

The partial functions are used in some places of the Scala's API. Maybe one of the most common example is the scala.collection.TraversableLike.collect(pf: PartialFunction[A, B]) used to select the elements of a collection satisfying the predicate expressed as a partial function. The predicate's filtering is in this case handled by PartialFunction's runWith[U](action: B => U). It was omitted before since it wraps 2 other described methods and can be expressed as: if(partialFunction.isDefinedAt(x)) { action(partialFunction(x)); true } else false.

However sometimes the partial functions are used inside unsafe (from the partial function's point of view) calls where no check on computed value is made. It can happen for instance when the partial function is used inside a map(...) function where one of mapped values is invalid. It's shown in one of next section's tests (e.g. "should fail when applied on foreach method without controlling the accepted parameters").

Partial functions examples

Below tests show how the partial functions can be used in Scala:

class PartialFunctionSpec extends FunSpec with Matchers {

  val sentEmails = new mutable.ListBuffer[String]()

  object EmailSender extends PartialFunction[String, Boolean] {
    val Blacklisted = Seq("test@blacklisted-email", "test@blacklisted-email-com")
    override def isDefinedAt(email: String): Boolean = !EmailSender.Blacklisted.contains(email)

    override def apply(email: String): Boolean = {
      sentEmails.append(email)
      true
    }
  }
  val blacklistedEmailHandler = new PartialFunction[String, String] {
    override def isDefinedAt(email: String): Boolean = EmailSender.Blacklisted.contains(email)

    override def apply(email: String): String = "blacklisted"
  }

  describe("chained partial function") {
    it("should apply the default method when the parameter is ignored") {
      val senderWithDefaultBehavior = EmailSender orElse blacklistedEmailHandler
      val blacklistedEmailResult = senderWithDefaultBehavior(EmailSender.Blacklisted.head)

      blacklistedEmailResult shouldEqual "blacklisted"
    }
    it("should apply the partial function when the parameter is accepted") {
      val senderWithDefaultBehavior = EmailSender orElse blacklistedEmailHandler
      val blacklistedEmailResult = senderWithDefaultBehavior("normal-email@email")

      blacklistedEmailResult shouldEqual true
    }
    it("should apply the next method to the result") {
      var messageDisplayer = new PartialFunction[Boolean, String] {
        override def isDefinedAt(sendResult: Boolean): Boolean = true

        override def apply(sendResult: Boolean): String =
          if (sendResult) "Your e-mail was sent" else "An error occured on sending your e-mail"
      }
      val mailSenderWithMessage = EmailSender andThen messageDisplayer

      val message = mailSenderWithMessage("my@mail")

      message shouldEqual "Your e-mail was sent"
    }
    it("should apply an action after running the partial function with valid argument") {
      var wasExecuted = false
      val senderWithResultHandler = EmailSender.runWith(_ => wasExecuted = true)

      val result = senderWithResultHandler("not-blacklisted@email")

      wasExecuted shouldEqual true
      result shouldEqual true
    }
    it("should apply an action after running the partial function with invalid argument") {
      var wasExecuted = false
      val senderWithResultHandler = EmailSender.runWith(_ => wasExecuted = true)

      val result = senderWithResultHandler(EmailSender.Blacklisted.head)

      wasExecuted shouldEqual false
      result shouldEqual false
    }
  }
  describe("lifted partial function") {
    it("should return an empty object where the parameter is not accepted") {
      val liftedEmailSender = EmailSender.lift

      val blacklistedResult = liftedEmailSender(EmailSender.Blacklisted.head)

      blacklistedResult shouldEqual None
    }
    it("should return the evaluation result where the parameter is accepted") {
      val liftedEmailSender = EmailSender.lift

      val acceptedEmailResult = liftedEmailSender("accepted@email")

      acceptedEmailResult shouldEqual Some(true)
    }
  }
  describe("partial function") {
    it("should apply correctly with collect method") {
      val emails = Seq(EmailSender.Blacklisted.head, "accepted@email", "accepted2@email")

      val collectedResults = emails.collect(EmailSender)

      collectedResults should have size 2
      collectedResults should contain only (true)
    }
    it("should doesn't apply partial function correctly with map") {
      val emails = Seq(EmailSender.Blacklisted.head, "accepted@email", "accepted2@email")

      val mappedEmails = emails.map(EmailSender)

      mappedEmails should have size 3
      // As you can notice, even if the list contains a blacklisted e-mail, it's mapped to true (apply method is
      // always called)
      mappedEmails should contain only (true)
    }
    it("should apply partial function or its alternative with map") {
      val emails = Seq(EmailSender.Blacklisted.head, "accepted@email", "accepted2@email")

      val mappedEmails = emails.map(EmailSender orElse blacklistedEmailHandler)

      mappedEmails should have size 3
      mappedEmails should contain allOf(true, "blacklisted")
    }
    it("should apply the partial function with anonymous function notation and pattern matching") {
      val numbers = 1 to 5

      // as already told, the collect checks for the applicability of the parameters
      // check the next test to see what happens if other method than collect is used
      val evenNumbersFiltered = numbers.collect {
        case nr: Int if nr%2 == 0 => nr
      }

      evenNumbersFiltered should have size 2
      evenNumbersFiltered should contain allOf(2, 4)
    }
    it("should fail when applied on foreach method without controlling the accepted parameters") {
      val numbers = 1 to 5

      val error = intercept[MatchError] {
        numbers.map {
          case nr: Int if nr%2 == 0 => nr
        }
      }

      error.getMessage() shouldEqual "1 (of class java.lang.Integer)"
    }
  }
}

The post presented the partial functions. The first section explained their main purpose being the control on the accepted values. The second part shown the API based on PartialFunction trait. It also listed the main methods that should be implemented in order to guarantee safe function execution (such as isDefinedAt, runWith). The last section gave some examples of the partial function's correct and incorrect use.