Chapter 12. built-in higher-order functions: Power Up Your Code

image

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.

Kotlin has a bunch of built-in higher-order functions

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:

image

We’ll start by looking at how to find the lowest or highest value in a collection of objects.

The min and max functions work with basic types

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. Ints can be arranged in numerical order, for example, which makes it easy to find out which Int has the highest value, and Strings can be arranged in alphabetical order.

image

The minBy and maxBy functions work with ALL types

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.

image

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:

image

And if you wanted to find the item with the lowest quantity value, you would use minBy:

image

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.

A closer look at minBy and maxBy’s lambda parameter

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.

What about minBy and maxBy’s return type?

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.

The sumBy and sumByDouble functions

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 Ints, and sumByDouble works with Doubles. 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.

image

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 }

sumBy and sumByDouble’s lambda parameter

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 the Groceries project

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:

image

Test drive

image

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

Meet the filter function

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”:

image

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 }

There’s a whole FAMILY of filter functions

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”:

Note

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

image

Now that you’ve seen how the filter function works, let’s look at another of Kotlin’s higher-order functions: the map function.

Use map to apply a transform to your collection

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.

Note

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:

image

And you can also use map to create a new List containing the name of each Grocery item in groceries:

image

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:

image

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.

You can chain function calls together

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:

image

Let’s go behind the scenes and see what happens when this code runs.

What happens when the code runs

  1. 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.

    image
  2. 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.

    image

The story continues...

  1. 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.

    image

Now that you’ve seen what happens when higher-order functions are chained together, let’s have a look at our next function: forEach.

forEach works like a for loop

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:

image

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.

image

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.

forEach has no return value

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.

Lambdas have access to variables

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:

image

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:

image

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.

Update the Groceries project

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):

image
image

Let’s take the code for a test drive.

Test drive

image

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.

Use groupBy to split your collection into groups

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

Note that you can’t use groupBy on a Map directly, but you can call it on its keys, values or entries properties.

image

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:

image

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>:

image

You can use groupBy in function call chains

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:

image

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:

image

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:

image

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.

How to use 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:

image

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:

image

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.

Behind the scenes: 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)
  1. 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.

    image
  2. 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.

    image
  3. 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.

    image
  4. 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.

    image
  5. 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.

    image

Some more examples of fold

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.

Find the product of a List<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:

image

Concatenate together the name of each item in a List<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:

image

Subtract the total price of items from an initial value

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:

image

Now that you know how to use the groupBy and fold functions, let’s update our project code.

Update the Groceries project

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):

image
image

Let’s take the code for a test drive.

Test drive

image

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

Your Kotlin Toolbox

You’ve got Chapter 12 under your belt and now you’ve added built-in higher-order functions to your toolbox.

Note

You can download the full code for the chapter from https://tinyurl.com/HFKotlin.

Leaving town...

image

It’s been great having you here in Kotlinville

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!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.220.160.216