Kotlin has an entire host of built-in higher-order functions.
And in this chapter, we’ll introduce you to some of the most useful ones. You’ll meet the flexible filter family , and discover how they can help you trim your collection down to size. You’ll learn how to transform a collection using map , loop through its items with forEach , and how to group the items in your collection using groupBy . You’ll even use fold to perform complex calculations using just one line of code. By the end of the chapter, you’ll be able to write code more powerful than you ever thought possible.
As we said at the beginning of the previous chapter, Kotlin comes with a bunch of built-in higher-order functions that take a lambda parameter, many of which deal with collections. They enable you to filter a collection based on some criteria, for example, or group the items in a collection by a particular property value.
Each higher-order function has a generalized implementation, and its specific behavior is defined by the lambda that you pass to it. So if you want to filter a collection using the built-in filter
function, you can specify the criteria that should be used by passing the function a lambda that defines it.
As many of Kotlin’s higher-order functions are designed to work with collections, we’re going to introduce you to some of the most useful higher-order functions defined in Kotlin’s collections package. We’ll explore these functions using a Grocery
data class, and a List
of Grocery
items named groceries
. Here’s the code to define them:
We’ll start by looking at how to find the lowest or highest value in a collection of objects.
As you already know, if you have a collection of basic types, you can use the min
and max
functions to find the lowest or highest value. If you want to find the highest value in a List<Int>
, for example, you can use the following code:
val ints = listOf(1, 2, 3, 4)
val maxInt = ints.max() //maxInt == 4
The min
and max
functions work with Kotlin’s basic types because they have a natural order. Int
s can be arranged in numerical order, for example, which makes it easy to find out which Int
has the highest value, and String
s can be arranged in alphabetical order.
The min
and max
functions, however, can’t be used with types with no natural order. You can’t use them, for example, with a List<Grocery>
or a Set<Duck>
, as the functions don’t automatically know how Grocery
items or Duck
objects should be ordered. This means that for more complex types, you need a different approach.
If you want to find the lowest or highest value of a type that’s more complex, you can use the minBy
and maxBy
functions. These functions work in a similar way to min
and max
, except that you can pass them criteria. You can use them, for example, to find the Grocery
item with the lowest unitPrice
or the Duck
with the greatest size
.
The minBy
and maxBy
functions each take one parameter: a lambda that tells the function which property it should use in order to determine which item has the lowest or highest value. If, for example, you wanted to find the item in a List<Grocery>
with the highest unitPrice
, you could do so using the maxBy
function like this:
And if you wanted to find the item with the lowest quantity
value, you would use minBy
:
The lambda expression that you pass to the minBy
or maxBy
function must take a specific form in order for the code to compile and work correctly. We’ll look at this next.
When you call the minBy
or maxBy
function, you must provide it with a lambda which takes the following form:
{ i: item_type -> criteria }
The lambda must have one parameter, which we’ve denoted above using i: item_type
. The parameter’s type must match the type of item that the collection deals with, so if you want to use either function with a List<Grocery>
, the lambda’s parameter must have a type of Grocery
:
{ i: Grocery -> criteria }
As each lambda has a single parameter of a known type, we can omit the parameter declaration entirely, and refer to the parameter in the lambda body using it
.
The lambda body specifies the criteria that should be used to determine the lowest—or highest—value in the collection. This criteria is normally the name of a property—for example, { it.unitPrice }
. It can be any type, just so long as the function can use it to determine which item has the lowest or highest property value.
minBy and maxBy work with collections that hold any type of object, making them much more flexible than min and max.
When you call the minBy
or maxBy
function, its return type matches the type of the items held in the collection. If you use minBy
with a List<Grocery>
, for example, the function will return a Grocery
. And if you use maxBy
with a Set<Duck>
, it will return a Duck
.
If you call minBy or maxBy on a collection that contains no items, the function will return a null value.
Now that you know how to use minBy
and maxBy
, let’s look at two of their close relatives: sumBy
and sumByDouble
.
As you may expect, the sumBy
and sumByDouble
functions return a sum of the items in a collection according to some criteria which you pass to it via a lambda. You can use these functions to, say, add together the quantity
values for each item in a List<Grocery>
, or return the sum of each unitPrice
multiplied by the quantity
.
sumBy adds Ints together, and returns an Int.
The sumBy
and sumByDouble
functions are almost identical, except that sumBy
works with Int
s, and sumByDouble
works with Double
s. To return the sum of a Grocery
’s quantity
values, for example, you would use the sumBy
function, as quantity
is an Int
:
sumByDouble adds Doubles, and returns a Double.
And to return the sum of each unitPrice
multiplied by the quantity
value, you would use sumByDouble
, as unitPrice * quantity
is a Double
:
val totalPrice = groceries.sumByDouble { it.quantity * it.unitPrice }
Just like minBy
and maxBy
, you must provide sumBy
and sumByDouble
with a lambda that takes this form:
{ i: item_type -> criteria }
As before, item_type
must match the type of item that the collection deals with. In the above examples, we’re using the functions with a List<Grocery>
, so the lambda’s parameter must have a type of Grocery
. As the compiler can infer this, we can omit the lambda parameter declaration, and refer to the parameter in the lambda body using it
.
The lambda body tells the function what you want it to sum. As we said above, this must be an Int
if you’re using the sumBy
function, and a Double
if you’re using sumByDouble
. sumBy
returns an Int
value, and sumByDouble
returns a Double
.
Now that you know how to use minBy
, maxBy
, sumBy
and sumByDouble
, let’s create a new project and add some code to it that uses these functions.
Create a new Kotlin project that targets the JVM, and name the project “Groceries”. Then create a new Kotlin file named Groceries.kt by highlighting the src folder, clicking on the File menu and choosing New → Kotlin File/Class. When prompted, name the file “Groceries”, and choose File from the Kind option.
Next, update your version of Groceries.kt to match ours below:
When we run the code, the following text gets printed in the IDE’s output window:
highestUnitPrice: Grocery(name=Olive oil, category=Pantry, unit=Bottle, unitPrice=6.0, quantity=1)
lowestQuantity: Grocery(name=Mushrooms, category=Vegetable, unit=lb, unitPrice=4.0, quantity=1)
sumQuantity: 9
totalPrice: 28.0
The next stop on our tour of Kotlin’s higher-order functions is filter
. This function lets you search, or filter, a collection according to some criteria that you pass to it using a lambda.
For most collections, filter
returns a List
that includes all the items that match your criteria, which you can then use elsewhere in your code. If it’s being used with a Map
, however, it returns a Map
. The following code, for example, uses the filter
function to get a List
of all the items in groceries
whose category
value is “Vegetable”:
Just like the other functions you’ve seen in this chapter, the lambda that you pass to the filter
function takes one parameter, whose type must match that of the items in the collection. As the lambda’s parameter has a known type, you can omit the parameter declaration, and refer to it in the lambda body using it
.
The lambda’s body must return a Boolean
, which is used for the filter
function’s criteria. The function returns a reference to all items from the original collection where the lambda body evaluates to true
. The following code, for example, returns a List
of Grocery
items whose unitPrice
is greater than 3.0:
val unitPriceOver3 = groceries.filter { it.unitPrice > 3.0 }
Kotlin has several variations of the filter
function that can sometimes be useful. The filterTo
function, for example, works like the filter
function, except that it appends the items that match the specified criteria to another collection. The filterIsInstance
function returns a List
of all the items which are instances of a given class. And the filterNot
function returns those items in a collection which don’t match the criteria you pass to it. Here’s how, for example, you would use the filterNot
function to return a List
of all Grocery
items whose category
value is not “Frozen”:
You can find out more about Kotlin’s filter family in the online documentation:
https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/index.html
Now that you’ve seen how the filter
function works, let’s look at another of Kotlin’s higher-order functions: the map
function.
The map
function takes the items in a collection, and transforms each one according to some formula that you specify. It returns the results of this transformation as a new List
.
Yes! The map function returns a List, and not a Map.
To see how this works, suppose you have a List<Int>
that looks like this:
val ints = listOf(1, 2, 3, 4)
If you wanted to create a new List<Int>
that contains the same items multiplied by two, you could do so using the map
function like this:
And you can also use map
to create a new List
containing the name of each Grocery
item in groceries
:
In each case, the map
function returns a new List
, and leaves the original collection intact. If, say, you use map
to create a List
of each unitPrice
multiplied by 0.5 using the following code, the unitPrice
of each Grocery
item in the original collection stays the same:
Just as before, the lambda that you pass to the map
function has a single parameter whose type matches that of the items in the collection. You can use this parameter (usually referred to using it
) to specify how you want each item in the collection to be transformed.
As the filter
and map
functions each return a collection, you can chain higher-order function calls together to concisely perform more complex operations. If you wanted to create a List
of each unitPrice
multiplied by two, where the original unitPrice
is greater than 3.0, you can do so by first calling the filter
function on the original collection, and then using map
to transform the result:
Let’s go behind the scenes and see what happens when this code runs.
val newPrices = groceries.filter { it.unitPrice > 3.0 }
.map { it.unitPrice * 2 }
The filter
function is called on groceries
, a List<Grocery>
. It creates a new List
that holds references to those Grocery
items whose unitPrice
is greater than 3.0.
val newPrices = groceries.filter { it.unitPrice > 3.0 }
.map { it.unitPrice * 2 }
The map
function is called on the new List
. As the lambda { it.unitPrice * 2 }
returns a Double
, the function creates a List<Double>
containing a reference to each unitPrice
multiplied by 2.
val newPrices = groceries.filter { it.unitPrice > 3.0 }
.map { it.unitPrice * 2 }
A new variable, newPrices
, gets created, and the reference to the List<Double>
returned by the map
function is assigned to it.
Now that you’ve seen what happens when higher-order functions are chained together, let’s have a look at our next function: forEach
.
The forEach
function works in a similar way to a for
loop, as it allows you to perform one or more actions against each item in a collection. You specify these actions using a lambda.
To see how forEach
works, suppose you wanted to loop through each item in the groceries List
, and print the name
of each one. Here’s how you could do this using a for
loop:
for (item in groceries) {
println(item.name)
}
And here’s the equivalent code using the forEach
function:
You can use forEach with arrays, Lists, Sets, and on a Map’s entries, keys and values properties.
Both code examples do the same thing, but using forEach
is slightly more concise.
As forEach
is a function, you can use it in function call chains.
Imagine that you want to print the name of each item in groceries whose unitPrice
is greater than 3.0. To do this using a for
loop, you could use the code:
for (item in groceries) {
if (item.unitPrice > 3.0) println(item.name)
}
But you can do this more concisely using:
groceries.filter { it.unitPrice > 3.0 }
.forEach { println(it.name) }
So forEach
lets you chain function calls together to perform powerful tasks in a way that’s concise.
Let’s take a closer look at forEach
.
Just like the other functions that you’ve seen in this chapter, the lambda that you pass to the forEach
function has a single parameter whose type matches that of the items in the collection. And as this parameter has a known type, you can omit the parameter declaration, and refer to the parameter in the lambda body using it
.
Unlike other functions, however, the lambda’s body has a Unit
return value. This means that you can’t use forEach
to return the result of some calculation as you won’t be able to access it. There is, however, a workaround.
As you already know, a for
loop’s body has access to variables that have been defined outside the loop. The following code, for example, defines a String
variable named itemNames
, which is then updated in a for
loop’s body:
When you pass a lambda to a higher-order function such as forEach
, the lambda has access to these same variables, even though they’ve been defined outside the lambda. This means that instead of using the forEach
function’s return value to get the result of some calculation, you can update a variable from inside the lambda body. The following code, for example, is valid:
The variables defined outside the lambda which the lambda can access are sometimes referred to as the lambda’s closure. In clever words, we say that the lambda can access its closure. And as the lambda uses the itemNames
variable in its body, we say that the lambda’s closure has captured the variable.
Now that you’ve learned how to use the forEach
function, let’s update our project code.
Closure means that a lambda can access any local variables that it captures.
We’ll add some code to our Groceries project that uses the filter
, map
and forEach
functions. Update your version of Groceries.kt in the project so that it matches ours below (our changes are in bold):
Let’s take the code for a test drive.
When we run the code, the following text gets printed in the IDE’s output window:
vegetables: [Grocery(name=Tomatoes, category=Vegetable, unit=lb, unitPrice=3.0, quantity=3),
Grocery(name=Mushrooms, category=Vegetable, unit=lb, unitPrice=4.0, quantity=1)]
notFrozen: [Grocery(name=Tomatoes, category=Vegetable, unit=lb, unitPrice=3.0, quantity=3),
Grocery(name=Mushrooms, category=Vegetable, unit=lb, unitPrice=4.0, quantity=1),
Grocery(name=Bagels, category=Bakery, unit=Pack, unitPrice=1.5, quantity=2),
Grocery(name=Olive oil, category=Pantry, unit=Bottle, unitPrice=6.0, quantity=1)]
groceryNames: [Tomatoes, Mushrooms, Bagels, Olive oil, Ice cream]
halfUnitPrice: [1.5, 2.0, 0.75, 3.0, 1.5]
newPrices: [8.0, 12.0]
Grocery names:
Tomatoes
Mushrooms
Bagels
Olive oil
Ice cream
Groceries with unitPrice > 3.0:
Mushrooms
Olive oil
itemNames: Tomatoes Mushrooms Bagels Olive oil Ice cream
Now that you’ve updated your project code, have a go at the following exercise, and then we’ll look at our next higher-order function.
The next function that we’ll look at is groupBy
. This function lets you group the items in your collection according to some criteria, such as the value of one of its properties. You can use it (in conjunction with other function calls) to, say, print the name of Grocery
items grouped by category
value:
Note that you can’t use groupBy on a Map directly, but you can call it on its keys, values or entries properties.
The groupBy
function accepts one parameter, a lambda, which you use to specify how the function should group the items in the collection. The following code, for example, groups the items in groceries
(a List<Grocery>
) by the category
value:
groupBy
returns a Map
. It uses the criteria passed via the lambda body for the keys, and each associated value is a List
of items from the original collection. The above code, for example, creates a Map
whose keys are the Grocery
item category
values, and each value is a List<Grocery>
:
As the groupBy
function returns a Map
with List
values, you can make further higher-order function calls on its return value, just as you can with the filter
and map
functions.
Imagine that you want to print the value of each category
for a List<Grocery>
, along with the name
of each Grocery
item whose category
property has that value. To do this, you can use the groupBy
function to group the Grocery
items by each category
value, and then use the forEach
function to loop through the resulting Map
:
As the groupBy
function uses the Grocery
category values for its keys, we can print them by passing the code println(it.key)
to the forEach
function in its lambda:
And as each of the Map
’s values is a List<Grocery>
, we can make a further call to forEach
in order to print the name of each grocery item:
So when you run the above code, it produces the following output:
Vegetable
Tomatoes
Mushrooms
Bakery
Bagels
Pantry
Olive oil
Frozen
Ice cream
Now that you know how to use groupBy
, let’s look at the final function on our road trip: the fold
function.
The fold
function is arguably Kotlin’s most flexible higher-order function. With fold
, you can specify an initial value, and perform some operation on it for each item in a collection. You can use it to, say, multiply together each item in a List<Int>
and return the result, or concatenate together the name of each item in a List<Grocery>
, all in a single line of code.
Unlike the other functions we’ve seen in this chapter, fold
takes two parameters: the initial value, and the operation that you want to perform on it, specified by a lambda. So if you have the following List<Int>
:
val ints = listOf(1, 2, 3)
you can use fold
to add each of its items to an initial value of 0 using the following code:
The fold
function’s first parameter is the initial value—in this case, 0. This parameter can be any type, but it’s usually one of Kotlin’s basic types such as a number or String
.
The second parameter is a lambda which describes the operation that you want to perform on the initial value for each item in the collection. In the above example, we want to add each item to the initial value, so we’re using the lambda:
The lambda that you pass to fold
has two parameters, which in this example we’ve named runningSum
and item
.
The first lambda parameter, runningSum
, gets its type from the initial value that you specify. It’s initialized with this initial value, so in the above example, runningSum
is an Int
that’s initialized with 0.
The second lambda parameter, item
, has the same type as the items in the collection. In the example above, we’re calling fold
on a List<Int>
, so item
’s type is Int
.
The lambda body specifies the operation you want to perform for each item in the collection, the result of which is then assigned to the lambda’s first parameter variable. In the above example, the function takes the value of runningSum
, adds it to the value of the current item, and assigns this new value to runningSum
. When the function has looped through all items in the collection, fold
returns the final value of this variable.
Let’s break down what happens when we call the fold
function.
Here’s what happens when we run the code:
val sumOfInts = ints.fold(0) { runningSum, item -> runningSum + item }
where ints
is defined as:
val ints = listOf(1, 2, 3)
val sumOfInts = ints.fold(0) { runningSum, item -> runningSum + item }
This creates an Int
variable named runningSum
which is initialized with 0. This variable is local to the fold
function.
val sumOfInts = ints.fold(0) { runningSum, item -> runningSum + item }
The function takes the value of the first item in the collection (an Int
with a value of 1) and adds it to the value of runningSum
. This new value, 1, is assigned to runningSum
.
val sumOfInts = ints.fold(0) { runningSum, item -> runningSum + item }
The function moves to the second item in the collection, which is an Int
with a value of 2. It adds this to runningSum
, so that runningSum
’s value becomes 3.
val sumOfInts = ints.fold(0) { runningSum, item -> runningSum + item }
The function moves to the third and final item in the collection: an Int
with a value of 3. This value is added to runningSum
, so that runningSum
’s value becomes 6.
val sumOfInts = ints.fold(0) { runningSum, item -> runningSum + item }
As there are no more items in the collection, the function returns the final value of runningSum
. This value is assigned to a new variable named sumOfInts
.
Now that you’ve seen how to use the fold
function to add together the values in a List<Int>
, let’s look at a few more examples.
<
Int>
If you want to multiply together all the numbers in a List<Int>
and return the result, you can do so by passing the fold
function an initial value of 1, and a lambda whose body performs the multiplication:
<
Grocery>
To return a String
that contains the name of each Grocery
item in a List<Grocery>
, you can pass the fold
function an initial value of ""
, and a lambda whose body performs the concatenation:
You can also use fold
to work out how much change you’d have left if you were to buy all the items in a List<Grocery>
. To do this, you’d set the initial value as the amount of money you have available, and use the lambda body to subtract the unitPrice
of each item multiplied by the quantity
:
Now that you know how to use the groupBy
and fold
functions, let’s update our project code.
We’ll add some code to our Groceries project that uses the groupBy
and fold
functions. Update your version of Groceries.kt in the project so that it matches ours below (our changes are in bold):
Let’s take the code for a test drive.
When we run the code, the following text gets printed in the IDE’s output window:
Vegetable
Tomatoes
Mushrooms
Bakery
Bagels
Pantry
Olive oil
Frozen
Ice cream
sumOfInts: 6
productOfInts: 6
names: Tomatoes Mushrooms Bagels Olive oil Ice cream
changeFrom50: 22.0
You’ve got Chapter 12 under your belt and now you’ve added built-in higher-order functions to your toolbox.
You can download the full code for the chapter from https://tinyurl.com/HFKotlin.
We’re sad to see you leave, but there’s nothing like taking what you’ve learned and putting it to use. There are still a few more gems for you in the back of the book and a handy index, and then it’s time to take all these new ideas and put them into practice. Bon voyage!
18.220.160.216