6 minutes read

Let's recall what we know about functions. We saw them operating on values, but those values were pure data that was already known and didn't require any computation to be evaluated, like numbers, strings, or booleans. But what prevents us from using functions themselves as values? And if any function can be assigned to a value, we can use it as an argument of some other function, or/and define a function that returns another function, right?

With all that in mind, we can now define a higher-order function (HOF) as a function that either takes another function as an argument or returns a function, or both.

Let's now have a look at how to use it in practice!

map

One of the most common use cases for higher-order functions is the map method. You can find it in many Scala standard library types, first of all, in various collections, like List, Set, Seq, and so on. map takes a function as a parameter and returns the same collection with the function applied to each of its elements. Note that the function you would like to apply has to be of type A => B, where A is the type of element of that particular collection on which you call the map method.

Here is a simple example:

val rawNumbers: List[String] = List("1", "2", "3")

def parseAndDouble(str: String): Int = str.toInt * 2

val result: List[Int] = rawNumbers.map(parseAndDouble) // List(2, 4, 6)

And here is a slightly more interesting one. Imagine you have a list of usernames in your system. You have been given a task to generate an email address in your company domain for each of them.

val users = List("Samuel", "William", "Jennifer")

def emailAssigner(username: String): String =
  username.toLowerCase + "@company.org"

You can easily do it using the map method and a function passed to it as an argument:

users.map(emailAssigner) // List([email protected], [email protected], [email protected])

Passing a function as an argument

It turns out that we have different requirements for emails in our system. Somewhere they have to be in lowercase, and somewhere else they need to be shortened if they are too long. How can we be flexible without creating several identical functions?
Let's create a special argument-function that formats the first part of the email:

def emailAssignerWith(username: String, prettify: String => String): String =
  prettify(username) + "@company.org"

Now let's create special functions for specific formats:

def lower(username: String): String = username.toLowerCase

def limitedLower(username: String): String = lower(username.take(5))

And then, combining them together, we get different results:

emailAssignerWith("Jennifer", lower) // [email protected]
emailAssignerWith("Jennifer", limitedLower) // [email protected]

Returning a function

Now imagine that our company uses separate domains for, say, separate departments. In this case, we need as many email generators as the number of departments and email domains we have. Sounds weird? And what about appending a new one each time a new department/domain appears? Even worse?

And if we need who-knows-how-many email generator functions producing email addresses, it's just natural to create a single function that will produce an email generator for any domain passed to it as an argument specific for that domain.

Let's do it:

def emailAssignerFor(domain: String, prettify: String => String): String => String =
  username => prettify(username) + "@" + domain

Now we can create email generators for different domains without any additional code:

val mainAssigner = emailAssignerFor("company.org", lower)
val subAssigner = emailAssignerFor("sub-company.ord", limitedLower)

// and usage
mainAssigner("Jennifer") // [email protected]
subAssigner("Jennifer") // [email protected]

That is especially handy if we have a list of data. We can simply apply the created email generator to all names at once:

val users = Seq("Samuel", "William", "Jennifer")

users.map(subAssigner) // List([email protected], [email protected], [email protected])

Conclusion

In this topic, you learned what higher-order functions are and how they differ from other functions. You have seen how using functions as arguments or return values can increase the flexibility and reusability of your code. It's time to put the use of higher-order functions into practice!

How did you like the theory?
Report a typo