The Neophyte's Guide to Scala Part 10: Staying DRY With Higher-order Functions
In the previous articles, I discussed the composable nature of Scala’s container types. As it turns out, being composable is a quality that you will not only find in Future
, Try
, and other container types, but also in functions, which are first class citizens in the Scala language.
Composability naturally entails reusability. While the latter has often been claimed to be one of the big advantages of object-oriented programming, it’s a trait that is definitely true for pure functions, i.e. functions that do not have any side-effects and are referentially transparent.
One obvious way is to implement a new function by calling already existing functions in its body. However, there are other ways to reuse existing functions: In this blog post, I will discuss some fundementals of functional programming that we have been missing out on so far. You will learn how to follow the DRY principle by leveraging higher-order functions in order to reuse existing code in new contexts.
On higher-order functions
A higher-order function, as opposed to a first-order function, can have one of three forms:
- One or more of its parameters is a function, and it returns some value.
- It returns a function, but none of its parameters is a function.
- Both of the above: One or more of its parameters is a function, and it returns a function.If you have followed this series, you have seen a lot of usages of higher-order functions of the first type: We called methods like
map
,filter
, orflatMap
and passed a function to it that was used to transform or filter a collection in some way. Very often, the functions we passed to these methods were anonymous functions, sometimes involving a little bit of duplication.
In this article, we are only concerned with what the other two types of higher-order functions can do for us: The first of them allows us to produce new functions based on some input data, whereas the other gives us the power and flexibility to compose new functions that are somehow based on existing functions. In both cases, we can eliminate code duplication.
And out of nowhere, a function was born
You might think that the ability to create new functions based on some input data is not terribly useful. While we mainly want to deal with how to compose new functions based on existing ones, let’s first have a look at how a function that produces new functions may be used.
Let’s assume we are implementing a freemail service where users should be able to configure when an email is supposed to be blocked. We are representing emails as instances of a simple case class:
case class Email(
subject: String,
text: String,
sender: String,
recipient: String)
We want to be able to filter new emails by the criteria specified by the user, so we have a filtering function that makes use of a predicate, a function of type Email => Boolean
to determine whether the email is to be blocked. If the predicate is true
, the email is accepted, otherwise it will be blocked:
type EmailFilter = Email => Boolean
def newMailsForUser(mails: Seq[Email], f: EmailFilter) = mails.filter(f)
Note that we are using a type alias for our function, so that we can work with more meaningful names in our code.
Now, in order to allow the user to configure their email filter, we can implement some factory functions that produce EmailFilter
functions configured to the user’s liking:
val sentByOneOf: Set[String] => EmailFilter =
senders => email => senders.contains(email.sender)
val notSentByAnyOf: Set[String] => EmailFilter =
senders => email => !senders.contains(email.sender)
val minimumSize: Int => EmailFilter = n => email => email.text.size >= n
val maximumSize: Int => EmailFilter = n => email => email.text.size <= n
Each of these four vals
is a function that returns an EmailFilter
, the first two taking as input a Set[String]
representing senders, the other two an Int
representing the length of the email body.
We can use any of these functions to create a new EmailFilter
that we can pass to the newMailsForUser
function:
val emailFilter: EmailFilter = notSentByAnyOf(Set("johndoe@example.com"))
val mails = Email(
subject = "It's me again, your stalker friend!",
text = "Hello my friend! How are you?",
sender = "johndoe@example.com",
recipient = "me@example.com") :: Nil
newMailsForUser(mails, emailFilter) // returns an empty list
This filter removes the one mail in the list because our user decided to put the sender on their black list. We can use our factory functions to create arbitrary EmailFilter
functions, depending on the user’s requirements.
Reusing existing functions
There are two problems with the current solution. First of all, there is quite a bit of duplication in the predicate factory functions above, when initially I told you that the composable nature of functions made it easy to stick to the DRY principle. So let’s get rid of the duplication.
To do that for the minimumSize
and maximumSize
, we introduce a function sizeConstraint
that takes a predicate that checks if the size of the email body is okay. That size will be passed to the predicate by the sizeConstraint
function:
type SizeChecker = Int => Boolean
val sizeConstraint: SizeChecker => EmailFilter = f => email => f(email.text.size)
Now we can express minimumSize
and maximumSize
in terms of sizeConstraint
:
val minimumSize: Int => EmailFilter = n => sizeConstraint(_ >= n)
val maximumSize: Int => EmailFilter = n => sizeConstraint(_ <= n)
Function composition
For the other two predicates, sentByOneOf
and notSentByAnyOf
, we are going to introduce a very generic higher-order function that allows us to express one of the two functions in terms of the other.
Let’s implement a function complement
that takes a predicate A => Boolean
and returns a new function that always returns the opposite of the given predicate:
def complement[A](predicate: A => Boolean) = (a: A) => !predicate(a)
Now, for an existing predicate p
we could get the complement by calling complement(p)
. However, sentByAnyOf
is not a predicate, but it returns one, namely an EmailFilter
.
Scala functions provide two composing functions that will help us here: Given two functions f
and g
, f.compose(g)
returns a new function that, when called, will first call g
and then apply f
on the result of it. Similarly, f.andThen(g)
returns a new function that, when called, will apply g
to the result of f
.
We can put this to use to create our notSentByAnyOf
predicate without code duplication:
val notSentByAnyOf = sentByOneOf andThen(g => complement(g))
What this means is that we ask for a new function that first applies the sentByOneOf
function to its arguments (a Set[String]
) and then applies the complement
function to the EmailFilter
predicate returned by the former function. Using Scala’s placeholder syntax for anonymous functions, we could write this more concisely as:
val notSentByAnyOf = sentByOneOf andThen(complement(_))
Of course, you will now have noticed that, given a complement
function, you could also implement the maximumSize
predicate in terms of minimumSize
instead of extracting a sizeConstraint
function. However, the latter is more flexible, allowing you to specify arbitrary checks on the size of the mail body.
Composing predicates
Another problem with our email filters is that we can currently only pass a single EmailFilter
to our newMailsForUser
function. Certainly, our users want to configure multiple criteria. We need a way to create a composite predicate that returns true
if either any, none or all of the predicates it consists of return true
.
Here is one way to implement these functions:
def any[A](predicates: (A => Boolean)*): A => Boolean =
a => predicates.exists(pred => pred(a))
def none[A](predicates: (A => Boolean)*) = complement(any(predicates: _*))
def every[A](predicates: (A => Boolean)*) = none(predicates.view.map(complement(_)): _*)
The any
function returns a new predicate that, when called with an input a
, checks if at least one of its predicates holds true for the value a
. Our none
function simply returns the complement of the predicate returned by any
– if at least one predicate holds true, the condition for none
is not satisfied. Finally, our every
function works by checking that none of the complements to the predicates passed to it holds true.
We can now use this to create a composite EmailFilter
that represents the user’s configuration:
val filter: EmailFilter = every(
notSentByAnyOf(Set("johndoe@example.com")),
minimumSize(100),
maximumSize(10000)
)
Composing a transformation pipeline
As another example of function composition, consider our example scenario again. As a freemail provider, we want not only to allow user’s to configure their email filter, but also do some processing on emails sent by our users. These are simple functions Email => Email
. Some possible transformations are the following:
val addMissingSubject = (email: Email) =>
if (email.subject.isEmpty) email.copy(subject = "No subject")
else email
val checkSpelling = (email: Email) =>
email.copy(text = email.text.replaceAll("your", "you're"))
val removeInappropriateLanguage = (email: Email) =>
email.copy(text = email.text.replaceAll("dynamic typing", "**CENSORED**"))
val addAdvertismentToFooter = (email: Email) =>
email.copy(text = email.text + "\nThis mail sent via Super Awesome Free Mail")
Now, depending on the weather and the mood of our boss, we can configure our pipeline as required, either by multiple andThen
calls, or, having the same effect, by using the chain
method defined on the Function
companion object:
val pipeline = Function.chain(Seq(
addMissingSubject,
checkSpelling,
removeInappropriateLanguage,
addAdvertismentToFooter))
Higher-order functions and partial functions
I won’t go into detail here, but now that you know more about how you can compose or reuse functions by means of higher-order functions, you might want to have a look at partial functions again.
Chaining partial functions
In the article on pattern matching anonymous functions, I mentioned that partial functions can be used to create a nice alternative to the chain of responsibility pattern: The orElse
method defined on the PartialFunction
trait allows you to chain an arbitrary number of partial functions, creating a composite partial function. The first one, however, will only pass on to the next one if it isn’t defined for the given input. Hence, you can do something like this:
val handler = fooHandler orElse barHandler orElse bazHandler
Lifting partial functions
Also, sometimes a PartialFunction
is not what you need. If you think about it, another way to represent the fact that a function is not defined for all input values is to have a standard function whose return type is an Option[A]
– if the function is not defined for an input value, it will return None
, otherwise a Some[A]
.
If that’s what you need in a certain context, given a PartialFunction
named pf
, you can call pf.lift
to get the normal function returning an Option
. If you have one of the latter and require a partial function, call Function.unlift(f)
.
Summary
In this article, we have seen the value of higher-order functions, which allow you to reuse existing functions in new, unforeseen contexts and compose them in a very flexible way. While in the examples, you didn’t save much in terms of lines of code, because the functions I showed were rather tiny, the real point is to illustrate the increase in flexibility. Also, composing and reusing functions is something that has benefits not only for small functions, but also on an architectural level.
In the next article, we will continue to examine ways to combine functions by means of partial function application and currying.
Posted by Daniel Westheide