![]() |
From Kotlin in Action by Dmitry Jemerov and Svetlana Isakova
The key idea of this article is the concept of higher-order functions. A higher-order function is a function that takes another function as an argument or returns one. |
Save 37% on Kotlin in Action. Just enter code fccjemerov into the discount code box at checkout at manning.com.
In Kotlin, functions can be represented as values using lambdas or function references. Therefore, a higher-order function is any function to which you can pass a lambda or a function reference as an argument, or a function which returns one, or both. For example, the filter
standard-library function takes a predicate function as an argument and is therefore a higher-order function:
list.filter { x > 0 }
Many higher-order functions are declared in the Kotlin standard library: map
, with
, and others. Now you’ll learn how to declare such functions in your own code. To do this, you must first be introduced to function types.
Function types
To declare a function that takes a lambda as an argument, you need to know how to declare the type of the corresponding parameter. Before we get to this, let’s look at a simpler case and store a lambda in a local variable. You already saw how you can do this without declaring the type, relying on Kotlin’s type inference:
val sum = { x: Int, y: Int -> x + y }
val action = { println(42) }
In this case, the compiler infers that both the sum
and action
variables have function types. Now let’s see what an explicit type declaration for these variables looks like:
val sum: (Int, Int) -> Int = { x, y -> x + y } ❶
val action: () -> Unit = { println(42) } ❷
❶ Function that takes two Int parameters and returns an Int value
❷ Function that takes no arguments and doesn’t return a value
To declare a function type, you put the function parameter types in parentheses, followed by an arrow and the return type of the function (see figure 1).
Figure 1. Function-type syntax in Kotlin
As you remember, the Unit
type is used to specify that a function returns no meaningful value. The Unit
return type can be omitted when you declare a regular function, but a function type declaration always requires an explicit return type, and you can’t omit Unit
in this context.
Note how you can omit the types of the parameters x, y
in the lambda expression { x, y \-> x + y }
. Because they’re specified in the function type as part of the variable declaration, you don’t need to repeat them in the lambda itself.
As with any other function, the return type of a function type can be marked as nullable:
var canReturnNull: (Int, Int) -> Int? = { null }
You can also define a nullable variable of a function type. To specify that the variable itself, rather than the return type of the function, is nullable, you need to enclose the entire function type definition in parentheses and put the question mark after the parentheses:
var funOrNull: ((Int, Int) -> Int)? = null
Note the subtle difference between this example and the previous one. If you omit the parentheses, you’ll declare a function type with a nullable return type, and not a nullable variable of a function type.
Parameter names of function types
You can specify names for parameters of a function type:
fun performRequest(
url: String,
callback: (code: Int, content: String) -> Unit ❶
) {
/*...*/
}
>>> val url = "http://kotl.in"
>>> performRequest(url) { code, content -> /*...*/ } ❷
>>> performRequest(url) { code, page -> /*...*/ } ❸
❶ The function type now has named parameters
❷ You can use the names provided in the API as lambda argument names …
❸ … or you can change them
Parameter names don’t affect type matching. When you declare a lambda, you don’t have to use the same parameter names as the ones used in the function type declaration. The names improve readability of the code and can be used in the IDE for code completion.
Calling functions passed as arguments
Now that you know how to declare a higher-order function, let’s discuss how to implement one. The first example is as simple as possible and uses the same type declaration as the sum
lambda you saw earlier. The function performs an arbitrary operation on two numbers, 2 and 3, and prints the result.
Listing 1. Implementing a higher-order function
fun twoAndThree(operation: (Int, Int) -> Int) { ❶
val result = operation(2, 3) ❷
println("The result is $result")
}
>>> twoAndThree { a, b -> a + b }
The result is 5
>>> twoAndThree { a, b -> a * b }
The result is 6
❶ Declares a parameter of a function type
❷ Calls the parameter of a function type
The syntax for calling the function passed as an argument is the same as calling a regular function – you put the parentheses after the function name and you put the parameters inside the parentheses.
As a more interesting example, let’s reimplement one of the most commonly used standard library functions: the filter
function. To keep things simple, you’ll implement the filter
function on String
, but the generic version that works on a collection of any elements is similar. Its declaration is shown in figure 2.
Figure 2. Declaration of the filter function, taking a predicate as a parameter
The filter
function takes a predicate as a parameter. The type of predicate
is a function that takes a character parameter and returns a boolean result. The result is true
if the character passed to the predicate needs to be present in the resulting string, or false
otherwise. Here’s how the function can be implemented:
Listing 2. Implementing the filter function
fun String.filter(predicate: (Char) -> Boolean): String {
val sb = StringBuilder()
for (index in 0 until length) {
val element = get(index)
if (predicate(element)) sb.append(element) ❶
}
return sb.toString()
}
>>> println("ab1c".filter { it in 'a'..'z' }) ❷
abc
❶ Calls the function passed as the argument for the “predicate” parameter
❷ Passes a lambda as an argument for “predicate”
The filter
function implementation is straightforward. It checks whether each character satisfies the predicate and, on success, adds it to the StringBuilder
containing the result.
Using function types from Java
Under the hood, function types are declared as regular interfaces; a variable of a function type is an implementation of a FunctionN
interface. The Kotlin standard library defines a series of interfaces, corresponding to different numbers of function arguments: Function0<R>
(this function takes no arguments), Function1<P1, R>
(this function takes one argument), etc. Each interface defines a single invoke
method, and calling it executes the function. A variable of a function type is an instance of a class implementing the corresponding FunctionN
interface, with the invoke
method containing the body of the lambda.
Kotlin functions that use function types can be called from Java. Java 8 lambdas are automatically converted to values of function types:
/* Kotlin declaration */
fun processTheAnswer(f: (Int) -> Int) {
println(f(42))
}
/* Java */
>>> processTheAnswer(number -> number + 1);
43
In older Java versions, you can pass an instance of an anonymous class implementing the invoke
method from the corresponding function interface:
/* Java */
>>> processTheAnswer(
... new Function1<Integer, Integer>() { ❶
... @Override
... public Integer invoke(Integer number) {
... System.out.println(number);
... return number + 1;
... }
... });
43
❶ Uses the Kotlin function type from Java code (prior to Java 8)
In Java, you can use extension functions from the Kotlin standard library that expect lambdas as arguments. Note that they don’t look as nice as in Kotlin—you must pass a receiver object as a first argument explicitly:
/* Java */
>>> List<String> strings = new ArrayList();
>>> strings.add("42");
>>> CollectionsKt.forEach(strings, s -> { ❶
... System.out.println(s);
... return Unit.INSTANCE; ❷
... });
❶ You can use a function from the Kotlin standard library in Java code
❷ You must return a value of Unit type explicitly
In Java, your function or lambda can return Unit
, but because the Unit
type has a value in Kotlin, you need to return it explicitly. You can’t pass a lambda returning void
as an argument of a function type that returns Unit
, like (String
) -> Unit
in the previous example.
Default and null values for parameters with function types
When you declare a parameter of a function type, you can also specify its default value. To see where this can be useful, let’s go back to an example of a joinToString
function. Here’s the implementation.
Listing 3. Implementation of a joinToString function
fun <T> Collection<T>.joinToString(
separator: String = ", ",
prefix: String = "",
postfix: String = ""
): String {
val result = StringBuilder(prefix)
for ((index, element) in this.withIndex()) {
if (index > 0) result.append(separator)
result.append(element) ❶
}
result.append(postfix)
return result.toString()
}
❶ Converts the object to a string using the default toString
method
This implementation is flexible, but it doesn’t let you control one key aspect of the conversion: how individual values in the collection are converted to strings. The code uses StringBuilder.append(o: Any?)
, which always converts the object to a string using the toString
method. This is good in a lot of cases, but not always. You now know that you can pass a lambda to specify how values are converted into strings, but requiring all callers to pass that lambda would be cumbersome, because most of them are okay with the default behavior. To solve this, you can define a parameter of a function type and specify a default value for it as a lambda.
Listing 4. Defining the parameter and specifying the default value
fun <T> Collection<T>.joinToString(
separator: String = ", ",
prefix: String = "",
postfix: String = "",
transform: (T) -> String = { it.toString() } ❶
): String {
val result = StringBuilder(prefix)
for ((index, element) in this.withIndex()) {
if (index > 0) result.append(separator)
result.append(transform(element)) ❷
}
result.append(postfix)
return result.toString()
}
>>> val letters = listOf("Alpha", "Beta")
>>> println(letters.joinToString()) ❸
Alpha, Beta
>>> println(letters.joinToString { it.toLowerCase() }) ❹
alpha, beta
>>> println(letters.joinToString(separator = "! ", postfix = "! ",
... transform = { it.toUpperCase() })) ❺
ALPHA! BETA!
❶ Declares a parameter of a function type with a lambda as a default value
❷ Calls the function passed as an argument for the “transform” parameter
❸ Uses the default conversion function
❹ Passes a lambda as an argument
❺ Uses the named argument syntax for passing several arguments, including a lambda
Note that this function is generic: it has a type parameter T
denoting the type of the element in a collection. The transform
lambda receives an argument of that type.
Declaring a default value of a function type requires no special syntax—you put the value as a lambda after the =
sign. The examples show different ways of calling the function: omitting the lambda entirely (the default toString()
conversion is used); passing it outside of the parentheses; and passing it as a named argument.
An alternative approach is to declare a parameter of a nullable function type. Note that you can’t call the function passed in such a parameter directly – Kotlin will refuse to compile such code, because it detects the possibility of null
pointer exceptions in this case. One option is to check for null explicitly:
fun foo(callback: (() -> Unit)?) {
// ...
if (callback != null) {
callback()
}
}
A shorter version makes use of the fact that a function type is an implementation of an interface with an invoke
method. As a regular method, invoke
can be called through the safe-call syntax: callback?.invoke()
.
Here’s how you can use this technique to rewrite the joinToString
function.
Listing 5. Rewriting the joinToString function
fun <T> Collection<T>.joinToString(
separator: String = ", ",
prefix: String = "",
postfix: String = "",
transform: ((T) -> String)? = null ❶
): String {
val result = StringBuilder(prefix)
for ((index, element) in this.withIndex()) {
if (index > 0) result.append(separator)
val str = transform?.invoke(element) ❷
?: element.toString() ❸
result.append(str)
}
result.append(postfix)
return result.toString()
}
❶ Declares a nullable parameter of a function type
❷ Uses the safe-call syntax to call the function
❸ Uses the Elvis operator to handle the case when a callback isn’t specified
Now you know how to write functions that take functions as arguments. Let’s look next at the other kind of higher-order functions: functions that return other functions.
Returning functions from functions
The requirement to return a function from another function doesn’t come up as often as passing functions to other functions, but it’s still useful. For instance, imagine a piece of logic in a program that can vary depending on the state of the program or other conditions—for example, calculating the cost of shipping depending on the selected shipping method. You can define a function that chooses the appropriate logic variant and returns it as another function. Here’s how this looks as code.
Listing 6. Returning a function from a function
enum class Delivery { STANDARD, EXPEDITED }
class Order(val itemCount: Int)
fun getShippingCostCalculator(
delivery: Delivery): (Order) -> Double { ❶
if (delivery == Delivery.EXPEDITED) {
return { order -> 6 + 2.1 * order.itemCount } ❷
}
return { order -> 1.2 * order.itemCount } ❷
}
>>> val calculator = ❸
... getShippingCostCalculator(Delivery.EXPEDITED)
>>> println("Shipping costs ${calculator(Order❸ )}") ❹
Shipping costs 12.3
❶ Declares a function that returns a function
❷ Returns lambdas from the function
❸ Stores the returned function in a variable
❹ Invokes the returned function
To declare a function that returns another function, you specify a function type as its return type. In listing 6, getShippingCostCalculator
returns a function that takes an Order
and returns a Double
. To return a function, you write a return
expression followed by a lambda, a member reference, or another expression of a function type, such as a local variable.
Let’s see another example where returning functions from functions is useful. Suppose you’re working on a GUI contact-management application, and you need to determine which contacts should be displayed, based on the state of the UI. Let’s say the UI allows you to type a string and then shows only contacts with names starting with that string; it also lets you hide contacts that don’t have a phone number specified. You’ll use the ContactListFilters
class to store the state of the options.
Listing 7. Using the ContactListFilters class to store the state of the options
class ContactListFilters {
var prefix: String = ""
var onlyWithPhoneNumber: Boolean = false
}
When a user types D
to see the contacts whose first or last name starts with D, the prefix
value is updated. We’ve omitted the code that makes the necessary changes – a full UI application would be too much code for an article, so we show a simplified example.
To decouple the contact-list display logic from the filtering UI, you can define a function that creates a predicate used to filter the contact list. This predicate checks the prefix and checks that the phone number is present, if required.
Listing 8. Predicate checking if the prefix and phone number are present
data class Person(
val firstName: String,
val lastName: String,
val phoneNumber: String?
)
class ContactListFilters {
var prefix: String = ""
var onlyWithPhoneNumber: Boolean = false
fun getPredicate(): (Person) -> Boolean { ❶
val startsWithPrefix = { p: Person ->
p.firstName.startsWith(prefix) || p.lastName.startsWith(prefix)
}
if (!onlyWithPhoneNumber) {
return startsWithPrefix ❷
}
return { startsWithPrefix(it)
&& it.phoneNumber != null } ❸
}
}
>>> val contacts = listOf(Person("Dmitry", "Jemerov", "123-4567"),
... Person("Svetlana", "Isakova", null))
>>> val contactListFilters = ContactListFilters()
>>> with (contactListFilters) {
>>> prefix = "Dm"
>>> onlyWithPhoneNumber = true
>>> }
>>> println(contacts.filter(
... contactListFilters.getPredicate())) ❹
[Person(firstName=Dmitry, lastName=Jemerov, phoneNumber=123-4567)]
❶ Declares a function that returns a function
❷ Returns a variable of a function type
❸ Returns a lambda from this function
❹ Passes the function returned by getPredicate as an argument to “filter”
The getPredicate
method returns a function value that you pass to the filter
function as an argument. Kotlin function types allow you to do this for values of other types, such as strings.
Higher-order functions give you an extremely powerful tool for improving the structure of your code and removing duplication. Let’s see how lambdas can help extract repeated logic from your code.
Removing duplication through lambdas
Function types and lambda expressions together constitute a great tool to create reusable code. Many kinds of code duplication that previously could be avoided only through cumbersome constructions can now be eliminated by using succinct lambda expressions.
Let’s look at an example that analyzes visits to a website. The class SiteVisit
stores the path of each visit, its duration, and the user’s OS. Various OSs are represented with an enum.
Listing 9. Example analyzing visits to a website using SiteVisit
data class SiteVisit(
val path: String,
val duration: Double,
val os: OS
)
enum class OS { WINDOWS, LINUX, MAC, IOS, ANDROID }
val log = listOf(
SiteVisit("/", 34.0, OS.WINDOWS),
SiteVisit("/", 22.0, OS.MAC),
SiteVisit("/login", 12.0, OS.WINDOWS),
SiteVisit("/signup", 8.0, OS.IOS),
SiteVisit("/", 16.3, OS.ANDROID)
)
Imagine that you need to display the average duration of visits from Windows machines. You can perform the task using the average
function.
Listing 10. Using the average function to display average duration of visits
val averageWindowsDuration = log
.filter { it.os == OS.WINDOWS }
.map(SiteVisit::duration)
.average()
>>> println(averageWindowsDuration)
23.0
Now, suppose you need to calculate the same statistics for Mac users. To avoid duplication, you can extract the platform as a parameter.
Listing 11. Extracting the platform as a parameter
fun List<SiteVisit>.averageDurationFor(os: OS) = ❶
filter { it.os == os }.map(SiteVisit::duration).average()
>>> println(log.averageDurationFor(OS.WINDOWS))
23.0
>>> println(log.averageDurationFor(OS.MAC))
22.0
❶ Duplicated code extracted into the function
Note how making this function an extension improves readability. You can even declare this function as a local extension function if it makes sense only in the local context.
But it’s not powerful enough. Imagine that you’re interested in the average duration of visits from the mobile platforms (currently you recognize two of them: iOS and Android).
Listing 12. Average visits from mobile platforms
val averageMobileDuration = log
.filter { it.os in setOf(OS.IOS, OS.ANDROID) }
.map(SiteVisit::duration)
.average()
>>> println(averageMobileDuration)
12.15
Now, a simple parameter representing the platform doesn’t do the job. It’s also likely that you’ll want to query the log with more complex conditions, such as “What’s the average duration of visits to the signup page from iOS?” Lambdas can help here. You can use function types to extract the required condition into a parameter.
Listing 13. Extracting the required condition into a parameter with function types
fun List<SiteVisit>.averageDurationFor(predicate: (SiteVisit) -> Boolean) =
filter(predicate).map(SiteVisit::duration).average()
>>> println(log.averageDurationFor {
... it.os in setOf(OS.ANDROID, OS.IOS) })
12.15
>>> println(log.averageDurationFor {
... it.os == OS.IOS && it.path == "/signup" })
8.0
Function types can help eliminate code duplication. If you’re tempted to copy and paste a piece of the code, it’s likely that the duplication can be avoided. With lambdas, you can extract not only the repeated data, but the behavior as well.
If you’re interested in learning more about the inner workings of Kotlin, check out the whole book on liveBook here and see this Slideshare presentation.