Higher-order functions are a feature of functional programming paradigm. As more and more languages are adopting functional paradigm, Kotlin is no behind.
According to Wikipedia,
In mathematics and computer science, a higher-order function is a function that does at least one of the following:
- takes one or more functions as arguments
- returns a function as its result.
So we can not only pass primitive and object types as arguments but also “functions” as arguments
Composing
Composing higher-order functions or using them can be confusing in the beginning. In this article, I intend to fix that.
Kotlin Standard Library is full of higher-order functions, from Scope Functions to Collections to Coroutines, they’re everywhere.
One such function you’ll find yourself using regularly with Collections
is filter()
.
filter()
source code looks like the following:
/**
* Returns a list containing only elements matching the given [predicate].
*/
public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
return filterTo(ArrayList<T>(), predicate)
}
Let’s say we are using filter()
on a List<String>
, then the function can be visualised as follows:
public inline fun <String> Iterable<String>.filter(predicate: (String) -> Boolean): List<String> {
return filterTo(ArrayList<String>(), predicate)
}
predicate: (String) -> Boolean
is the function we’re passing to a higher-order function filter()
. It can be visualised as a regular function in the following way:
fun filterPredicate(arg: String): Boolean { /* return result */ }
predicate: (String) -> Boolean
consists of 3 parts:
predicate
-> denotes the name of the argument passed tofilter()
. There is nothing special about it and is similar to what we do in regular functions. We could name it whatever we like.(String)
-> denotes the arguments required to the function we’ve passed to the higher-order function. If no arguments required then it will be empty()
.Boolean
-> denotes the return value of our passed function.
Another variation to filter()
is filterIndexed()
and it’s List<String>
visualisation is as follows:
/**
* Returns a list containing only elements matching the given [predicate].
* @param [predicate] function that takes the index of an element and the element itself
* and returns the result of predicate evaluation on the element.
*/
public inline fun <String> Iterable<String>.filterIndexed(predicate: (index: Int, String) -> Boolean): List<String> {
return filterIndexedTo(ArrayList<String>(), predicate)
}
If we see in predicate: (index: Int, String) -> Boolean
instead of taking a single argument, passed function to filterIndexed()
takes 2 arguments i.e. Int & String(naming these arguments are optional)
Passing a function
Functions in functional programming and Kotlin are considered as first-class citizens. It enables functions or function references to be passed around like any other value, also it can be associated with a variable i.e. val myFunction = /* function */
3 ways in which we can work with Higher-order functions:
- Lambdas
- Anonymous functions
- Function references
Consider the following list for further explanation:
val fruits = listOf("Orange","Apple","Mango","Peach","Watermelon")
Lambdas
Lambdas are the most common and cleanest way of working with higher-order functions.
Applying filter()
on fruits
using lambdas:
fruits.filter( { fruit -> fruit.length == 6 } ) // return Orange
In Lambdas, the value of the last expression is returned by default if not explicitly specified. In the above case, fruit.length == 6
resolves to return a Boolean value.
Anonymous Functions
As the name suggests, anonymous functions are nameless functions. Syntax to declare anonymous functions is similar to declaring regular functions but without a name.
Modifying the above example to use anonymous functions:
fruits.filter( fun(fruit): Boolean { return fruit.length == 6 } ) //returns Orange
Notice the syntax of the highlighted code snippet. It is required to explicitly return from anonymous functions.
Function References
Let’s say we have a member function already declared in our class, which takes fruit as a string argument and returns if it is a citrus fruit.
fun isCitrus(fruit: String): Boolean { return fruit == "Orange" }
We can pass this function into a higher-order function using :: operator
.
fruits.filter( ::isCitrus ) //returns Orange
One thing to make sure while using function references is that the signature of the function is correct.
filter()
takes the function of the type (String) -> Boolean
and our function isCitrus()
is also of the type (String) -> Boolean
Returning a function
Similarly, All 3 methods can be used to return functions as well. The syntax remains the same except for the function from which we are returning.
So let’s say for example purpose, we want to filter fruits based on time of the day.
fun getFilterCriteria(): (String) -> Boolean { val timeOfDay = getTime() //Imagine this working return when(timeOfDay){ isDay -> { //return fruit that keep doctor away { fruit:String -> fruit == "Apple"} // Lambdas } isAfternoon -> { //return something pulpy ::isCitrus // function references } isNight -> { //return gaint fruit fun(fruit:String):Boolean{return fruit=="Watermelon"}//Anonymous functions } else -> ::isCitrus } }
Now calling filter()
on fruits
:
fruits.filter(getFilterCriteria())
Notice getFilterCriteria()
returns (String) -> Boolean
which is basically function type and that’s how Kotlin allows returning functions.
Conclusion
Higher-order functions can bring great flexibility to our code, while also making code concise and easy to maintain.
A great exercise will be to go back to your code and see if you can change parts of it to harness the power of Higher-order functions.