**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.**

A virtual conference at the intersection of Data and AI. This is not a conference for the hype. Its real users talking about real experiences.

- 40+ speakers with the likes of Hannes from Duck DB, Sol Rashidi, Joe Reis, Sadie St. Lawrence, Ryan Wolf from nvidia, Rebecca from lidl

- 12th September 2024

- Three simultaneous tracks

- Panels, Lighting Talks, Keynotes, Booth crawls, Roundtables and Entertainment.

- Topics include (ingestion, finops for data, data for inference (feature platforms), data for ML observability

- 100% virtual and 100% free

đź‘‰ Register here

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.