1. Introduction
Kotlin strives to be a functional language, or rather, a language that allows the functional paradigm alongside the imperative one. The functional paradigm dictates that we should think of programs as a mosaic of functions, taking arguments and producing results. Having functional types and functional variables is an essential part of the paradigm, and of course, Kotlin supports it.
In this tutorial, we’ll discuss interfaces as method parameters and higher-order functions in Kotlin.
2. Declaring and Calling Higher-Order Functions
Declaring higher-order functions in Kotlin is simple enough. The most obvious way is to accept a lambda:
fun performAction(action: () -> Unit) {
// Do things before the action
action()
// Do things after the action
}
// Invocation:
performAction { println("Hello, world!") }
This example is not very functional, as the only type of function that can be passed as an argument is the type that accepts no arguments and produces no results, which pretty much narrows it down to various side-effects.
Let’s make it more interesting by returning a specific result. It’s also very tedious to type the full type definition of a lambda if we use the same argument type in multiple higher-order functions. Instead, we’ll use a type alias to shorten things:
typealias MySpecialSupplier = () -> SuppliedType
fun supplyWithTiming(supplier: MySpecialSupplier) =
measureTimedValue { supplier() }
.let {
println("Invocation took ${it.duration}ms") // Or there can be a Metrics call here
it.value // Returning the actual result
}
Finally, we might consider declaring a functional interface when there are many potential implementations for it. Alternatively, the authors of the higher-order function and users might be different groups of people, such as library authors and library users. Then we declare an interface with only one function and fun keyword in the front:
fun interface Mapper {
fun toSupply(name: String, weight: Int): SuppliedType
}
fun verifiedSupplier(name: String, weight: Int, mapper: Mapper): SuppliedType {
println("Some verification")
return mapper.toSupply(name, weight)
}
// Possible invocation:
verifiedSupplier("Sugar", 1) { name, weight -> SuppliedType(name, weight) }
As you can see, we are still able to pass a simple lambda as a parameter due to Kotlin SAM conversion.
Of course, sometimes our higher-order functions are generic enough not to care about lambda parameters or their returned type. In such cases, we would use generic types, like in this Kotlin standard library function:
public inline fun <T, R, C : MutableCollection<in R>> Iterable<T>
.mapTo(destination: C, transform: (T) -> R): C {
for (item in this)
destination.add(transform(item))
return destination
}
We can also do currying or partial application:
fun <T> curry(a: T, bifunction: (T, T) -> T): (T) -> T = { b -> bifunction(a, b) }
val plusFour = curry(4) { a, b -> a + b }
assert(plusFour(2) == 6)
This can be used for finer separation of concerns when the code invoking the function does not know where all the arguments for it come from.
3. Applications of Higher-Order Functions
There are times when we have to interrupt the business logic for technical or infrastructural reasons, such as metric instrumentation, logging, or resource usage. The resulting code is not very readable. It is also more brittle, as we may accidentally change business logic while changing non-business aspects of our code.
Let’s consider a piece of code that has to download a picture from a server within a timeout and handle possible exceptions that may arise:
val imageResponse = try {
clientExecutor.submit(Callable { client.get(imageUrl) })
.get(10L, TimeUnit.SECONDS)
} catch (ex: Exception) {
logger.error("Failed to get image: $imageUrl")
return
}
There is so much going on! We submit a task to an executor to be able to put a timeout on it. We catch a possible exception and make a log record. Moreover, this timeout code is likely repeated all over our service. We have to identify smaller patterns in this piece of code and bring them apart.
Let’s start with teasing out timeout functionality:
fun <T> timeout(timeoutSec: Long, supplier: Callable<T>): T =
clientExecutor.submit(supplier).get(timeoutSec, TimeUnit.SECONDS)
Now, if we ever want to put a timeout on a task, we can use this method. Next, let us deal with the exception logging aspect:
fun <T> successOrLogException(exceptionMsg: String, supplier: () -> T): T? = try {
supplier()
} catch (ex: Exception) {
logger.error(exceptionMsg)
null
}
Granted, in functional programming, there are more elegant ways to deal with an erroneous output, but this is just to illustrate the concept of concern separation with higher-order function. Let us apply the two created functions to the code:
val imageResponse = successOrLogException("Failed to get image: $imageUrl") {
timeout(10L) { client.get(imageUrl) }
} ?: return
The logic is identical, but now we are simply stating our intentions rather than painstakingly listing all the elementary steps needed to achieve them.
4. Conclusion
In this article, we looked at how we can create functions that take functions as arguments and return them as results. These functions are useful to write code as a series of declarative statements rather than a list of elementary instructions. Declarative statements disclose the reason behind the code. Such an approach helps to make the code more readable.
As usual, all the examples are available over on GitHub.