Chapters

Hide chapters

Functional Programming in Kotlin by Tutorials

First Edition · Android 12 · Kotlin 1.6 · IntelliJ IDEA 2022

Section I: Functional Programming Fundamentals

Section 1: 8 chapters
Show chapters Hide chapters

Appendix

Section 4: 13 chapters
Show chapters Hide chapters

5. Higher-Order Functions
Written by Massimo Carli

In the previous chapters, you learned that a lambda expression is basically a function that can be used as a value. This means you can define a lambda expression and assign it to a variable. More importantly, you can pass a lambda expression as an input parameter or return the value of another function. Passing lambdas as parameters describes higher-order functions, which are the main topic of this chapter.

Here, you’ll learn:

  • The difference between an imperative and declarative approach.
  • What higher-order functions are.
  • What a functional interface is and what SAM means.
  • Why interfaces with a single operation are fundamental in functional programming.
  • How inline works when using higher-order functions.

You’ll learn all this while using the Kotlin language and solving some fun exercises. Open up the starter project for this chapter in IntelliJ and keep reading.

Imperative vs. declarative approach

Before diving into the study of higher-order functions in Kotlin, it’s important to mention why they’re fundamental and why you should use them. One of the main reasons is that higher-order functions allow you to write your code in a declarative way.

But what does that mean? An example will make things easier.

Consider the following list of email address candidates that you’ll find in Imperative.kt in this chapter’s material:

val emails = listOf(
  "email@emmmaail.com",
  "max@fgh.it",
  "hsajdkjshh",
  "mike@mcarli.it",
  "first.second@ggg.com",
  "test@test.co.uk",
  "12345@qqq.com",
  "123.45",
  "12345@a.b.c.d",
  "fp_is_great@funprog.com",
  "aaaaaaaaa.bbbbb@cccc.com",
  "aaaaaaacccc.com",
  "valid@jjjj.lll",
)

Now, suppose you want to write a function that:

  1. Filters invalid email addresses.
  2. Uses only email addresses with more than 10 characters.
  3. Takes just the first five valid email addresses.

A first approach is the imperative one. In the same Imperative.kt file, write the following code:

fun imperative(emails: List<String>): List<String> = // 1
  mutableListOf<String>() // 2
    .apply {
      for (email in emails) { // 3
        if (EMAIL_REG_EX.matches(email) && email.length > 10) { // 4
          add(email)
          if (size >= 5) { // 5
            break
          }
        }
      }
    }    

In this code, you:

  1. Define imperative as a function that returns the first five valid email addresses longer than 10 characters.
  2. Initialize the List you’ll return as the result.
  3. Iterate over all the email addresses in input.
  4. Check if the email is valid using the available regular expression EMAIL_REG_EX. Here, you also check if the email has a minimum length of over 10.
  5. Stop when it gets 5 email addresses.

Now, add and run this:

fun main() {
  println(imperative(emails))
}

And you get the following output:

[email@emmmaail.com, mike@mcarli.it, first.second@ggg.com, test@test.co.uk, fp_is_great@funprog.com]

This is an example of an imperative approach or algorithmic programming. Here, you write the code specifying the steps the computer must take to accomplish the goal. In this case, you’re telling the computer exactly what steps to execute in a language it understands. If you want to change the criteria for filtering the emails, you need to first digest the code, apply the changes and update the unit tests. In this case, the goal you want to achieve is simple, but in many other cases, this could be a problem.

Note: For an even more imperative approach, you can replace the enhanced for loop:

for (email in emails){...}

With:

for (i in 0 until email.size){
 val email = emails[i]
 // ...
}

The declarative approach is closer to how humans think. Now, add the following code to Declarative.kt:

fun declarative(emails: List<String>): List<String> = // 1
  emails
    .filter { EMAIL_REG_EX.matches(it) } // 2
    .filter { it.length > 10 } // 3
    .take(5) // 4

In this case, you:

  1. Define declarative with the same signature as imperative.
  2. Use filter to remove the invalid email addresses.
  3. Use filter again to remove email addresses shorter than or equal to 10 characters.
  4. Take the first 5 email addresses using take.

Now, add and run the following code:

fun main() {
  println(declarative(emails))
}

And you get exactly the same output:

[email@emmmaail.com, mike@mcarli.it, first.second@ggg.com, test@test.co.uk, fp_is_great@funprog.com]

When you look at the code, you realize it’s just a description of the steps you want to do, which follows the initial requirements. You take five email addresses from the ones you filtered because they matched the email regular expression and were longer than 10 characters. You might already see some of the declarative approach’s advantages, but you can do even better than this.

Code readability

The declarative approach improves code readability. In the same Declarative.kt file, add the following code:

fun isEmailValid(email: String) = // 1
  EMAIL_REG_EX.matches(email)

fun isEmailLongEnough(email: String) = // 2
  email.length > 10

fun moreDeclarative(emails: List<String>): List<String> =
  emails
    .filter(::isEmailValid) // 3
    .filter(::isEmailLongEnough) // 4
    .take(5)

In this case, you:

  1. Define isEmailValid, which is a pure function that checks if the input email address is valid.
  2. Create isEmailLongEnough to check the length of the email address.
  3. Use the reference ::isEmailValid as the parameter of the first filter.
  4. Use ::isEmailLongEnough as the parameter of the second filter.

This is so declarative that you can read the code as if it were in normal, spoken English. You basically:

filter the valid emails
filter the emails that are long enough
take 5 of them

filter is a perfect example of a higher-order function, because it’s a function that accepts another function as an input parameter. Look at the source code for filter, and you’ll find the following:

public inline fun <T> Iterable<T>.filter(
  predicate: (T) -> Boolean
): List<T> {
  return filterTo(ArrayList<T>(), predicate)
}

Besides the implementation details, it’s important to see how:

  1. It’s an extension function for Iterable<T>.
  2. The input parameter is a predicate of type (T) -> Boolean. As you learned in Chapter 2, “Function Fundamentals”, a predicate is any function that returns a Boolean. In this case, the predicate is a lambda accepting an object of type T as input, which is the type parameter for Iterable<T>.

In the rest of the book, you’ll see many examples of this, and you’ll see how functional programming and the declarative approach get along. :]

Higher-order functions

By now you should know what a higher-order function is — you’ve already written some of them. The most important one you saw in Chapter 2, “Function Fundamentals” is:

inline infix fun <A, B, C> Fun<B, C>.after(
  crossinline f: Fun<A, B>
): Fun<A, C> = { a: A ->
  this(f(a)) // HERE
}

This is an extension function on Fun<B, C>, which receives as input another function of type Fun<A, B> and returns a function of type Fun<A, C>, which is the composition of the two.

In this next section, you’ll see some more examples of higher-order functions, which:

  • Accept a lambda expression as input.
  • Return a lambda expression as an output value.
  • Use lambda expressions as input and as output values.

You’ll also make everything fun with some nice exercises.

Accepting a lambda expression as input

You’ve already met one example of a higher-order function accepting a lambda expression as input. Now to write more. Open SimpleHoF.kt in the material for this project, and write the following code:

fun Int.times(fn: (Int) -> Unit) = (1..this).forEach(fn)

This function executes the lambda the provided number of times. In this code, you define:

  • times as an extension function for the Int type.
  • fn as an input parameter of type (Int) -> Unit, which is invoked the number of times described by the receiver. Remember that, in this case, the receiver is the Int value you invoke times on. In the case of 10.times{}, the receiver is 10.
  • The range as 1..this, and you iterate over it using forEach to execute fn that number of times.

You can test how times works by adding and running this:

fun main() {
  10.times {
    print(" $it")
  }
}

The output you get is:

 1 2 3 4 5 6 7 8 9 10

You probably think that, with the previous code, you cheated a little bit. :] forEach is another higher-order function Kotlin provides as an extension function for Iterable<T>, like the filter you met earlier:

public inline fun <T> Iterable<T>.forEach(action: (T) -> Unit): Unit {
    for (element in this) action(element)
}

OK, assuming you don’t want to use an existing higher-order function, you can replace the previous implementation with the following imperative-ish code:

fun Int.times(fn: (Int) -> Unit) {
  var i = 1
  while (i <= this) {
    fn(i++)
  }
}

When you run main, you get the same output:

 1 2 3 4 5 6 7 8 9 10

Exercise 5.1: Kotlin provides you with first, which returns the first element of Iterable<T> for which a predicate you provide in input evaluates to true. Remember that Iterable<T> is the abstraction of all the collections providing Iterator<T> implementation.

public interface Iterable<out T> {
  public operator fun iterator(): Iterator<T>
}

Iterator<T> allows you to iterate over all the elements of a collection in a way that doesn’t depend on the collection implementation itself:

public interface Iterator<out T> {

  public operator fun next(): T

  public operator fun hasNext(): Boolean
}

The current first signature is:

public inline fun <T> Iterable<T>.first(predicate: (T) -> Boolean): T

Kotlin doesn’t allow you to override the current extension function on Iterable<T>. So, how would you implement first on Array<T>?

The current implementation of first throws an exception if the collection is empty, so there’s no first T. How would you implement the function firstOrNull on Array<T> returning null in such a case?

Give it a try and check the challenge project or Appendix E to see how you did.

Implementing the strategy pattern in a functional way

Hey, isn’t this a book about functional programming? What does the strategy pattern have to do with this? As you know, design patterns describe a general and reusable solution to a commonly occurring problem within a given context. In particular, the strategy pattern is a behavioral pattern that defines a family of algorithms, encapsulates each one and makes them interchangeable. Sorting is a typical example. Open Strategy.kt and write the following code:

fun bubbleSort(values: IntArray) {
  for (i in values.size - 1 downTo 0) {
    for (j in 0 until i) {
      if (values[j] > values[j + 1]) {
        swap(values, j, j + 1)
      }
    }
  }
}

This function sorts an input IntArray using the bubble sort. Understanding how it’s sorting is less important here than knowing that it’s sorting Ints. Note, the swap function is already available in Util.kt.

You can test how it works by executing the following:

fun main() {
  val array = intArrayOf(10, 5, 2, 7, 8, 3)
  bubbleSort(array)
  array.printAll()
}

printAll is a function in Util.kt. Run this code, and you get what you expect:

 2 3 5 7 8 10

It’s important to note that you can use the bubble sort to sort anything, not just Int. What you need is just a way to understand when a value is greater than another value of the same type. How, then, can you abstract bubbleSort so you can use it with any array of values of type T?

Note: The bubble sort definitely isn’t the most performant sorting algorithm. It’s used here because it does not need a lot of code.

Note: If you want to learn more about algorithm and data structures in Kotlin, Data Structures & Algorithms in Kotlin is the best choice.

To solve this problem, you need to:

  • Make bubbleSort generic.
  • Provide bubbleSort what it’s missing: a way to understand if one T is greater than another T.

In the same file, replace the previous code with the following:

fun <T> bubbleSort(
  values: Array<T>, // 1
  isLarger: (T, T) -> Boolean // 2
) {
  for (i in values.size - 1 downTo 0) {
    for (j in 0 until i) {
      if (isLarger(values[j], values[j + 1])) { // 3
        swap(values, j, j + 1) // 4
      }
    }
  }
}

In this code, you:

  1. Define bubbleSort as a generic function with the type variable T.
  2. Pass the strategy as a lambda function of type (T, T) -> Boolean, which returns true if the first value is greater than the second.
  3. Use the isLarger lambda expression to compare each pair of values in the array.
  4. Swap the values, if needed, using the swap overload you find in Util.kt.

To test the previous code, write:

fun main() {
  val array = arrayOf(10, 5, 2, 7, 8, 3)
  bubbleSort(array) { first, second ->
    first > second
  }
  array.printAll()
}

Run this code, and you again get:

 2 3 5 7 8 10

Exercise 5.2: The command pattern is another important design pattern that defines abstractions like Command and CommandExecutor. Command abstracts every possible operation that CommandExecutor can run. In other words, a Command represents a task and you can pass a Command to a CommandExecutor to run it. How would you represent them in a functional way?

Optionally, can you also provide a way to “redo” the most recent Command?

Functional interfaces

In the previous chapters, you learned the concept of function type. If you consider the previous strategy pattern example, you could’ve defined the type for the predicate parameter using typealias, like this:

typealias IsLarger<T> = (T, T) -> Boolean

With this, the bubble sort example would become:

fun <T> bubbleSort(
  values: Array<T>,
  isLarger: IsLarger<T> // HERE
) {
  for (i in values.size - 1 downTo 0) {
    for (j in 0 until i) {
      if (isLarger(values[j], values[j + 1])) {
        swap(values, j, j + 1)
      }
    }
  }
}

As you learned in Chapter 2, “Function Fundamentals”, a function type is basically a way to represent a function in terms of:

  • Input parameters and their type
  • Return type

With (T, T) -> Boolean, you’re basically representing the type of all the functions with two parameters of type T returning a Boolean. With the typealias keyword, you just gave that type the name IsLarger<T>.

Kotlin allows you to do something similar with functional interfaces. You find the same concept with the acronym SAM, which stands for single abstract method. For instance, open FunctionalInterface.kt and write the following definition:

fun interface IsLarger<T> { // 1
  fun isLarger(a: T, b: T): Boolean // 2
}

In this case, you:

  1. Use the fun keyword as a qualifier for the IsLarger<T> interface.
  2. Define isLarger as the only abstract function for the functional interface.

Using the previous definition, you can add the following code to the same file:

fun <T> bubbleSortFI( // 1
  values: Array<T>,
  largerStrategy: IsLarger<T> // 2
) {
  for (i in values.size - 1 downTo 0) {
    for (j in 0 until i) {
      if (largerStrategy.isLarger(values[j], values[j + 1])) { // 3
        swap(values, j, j + 1)
      }
    }
  }
}

In this case, you:

  1. Create bubbleSortFI as a new version of the previous bubbleSort.
  2. Define the strategy as a parameter of type IsLarger<T>.
  3. Invoke isLarger, which is the operation you defined in IsLarger<T>.

Now, you can test bubbleSortFI in two different ways:

  • Using IsLarger<T> as a normal lambda expression.
  • Creating an IsLarger<T> implementation instance.

It’s useful to look at both.

Functional interface as a lambda expression

Because you defined IsLarger<T> as a functional interface, you can use the same syntax you’d use with lambda expressions. Add the following code to the same FunctionalInterface.kt file:

fun main() {
  val array = arrayOf(10, 5, 2, 7, 8, 3)
  bubbleSortFI(array) { first, second -> // HERE
    first > second
  }
  array.printAll()
}

Here, you pass the parameter of type IsLarger<T> as a normal lambda expression the same way you did when using typealias.

Functional interface instances

Again, because you defined IsLarger<T> as a functional interface, Kotlin provides a very handy syntax in case you want to create an implementation instance of the fly. Replace the previous code in FunctionalInterface.kt with:

fun main() {
  val array = arrayOf(10, 5, 2, 7, 8, 3)
  val largerStrategy = IsLarger<Int> { first, second -> // HERE
    first > second
  } 
  bubbleSortFI(array, largerStrategy)
  array.printAll()
}

Like in the previous example, although IsLarger<T> is an interface, you can quickly create an implementation instance using a syntax like:

   FunctionInterfaceType { /* Lambda expression for the SAM */}

So, what’s the difference between the definition of a function type using typealias and a functional interface?

Functional interface vs. typealias

It’s crucial to note how typealias doesn’t introduce a new type at all. It just allows you to give a different name to an existing type. On the other hand, when you define a functional interface, you’re creating a new type.

Note: Remember that typealias definitions aren’t visible from Java.

The difference between a simple interface and a functional interface is the presence of a single abstract method (SAM), which you provide using the same syntax you use for lambda expressions.

To better understand their differences, open FunctionalVsLambda.kt and add the following code:

typealias SinglePredicate<T> = (T) -> Boolean // 1

fun <T> SinglePredicate<T>.whoAmI() = println("I'm a typealias") // 2

fun main() {
  val isEven = { number: Int -> number % 2 == 0 } // 3
  isEven.whoAmI() // 4
}

Here, you just:

  1. Give the name SinglePredicate<T> to any function receiving a single parameter of type T and returning a Boolean.
  2. Define whoAmI as an extension function for SinglePredicate<T>, which just prints a message saying that the receiver type is typealias.
  3. Define the isEven lambda, which returns true if an Int value you pass as a parameter is even. Note how you’re just creating a lambda with no relation to the name SinglePredicate<T> you defined earlier. It’s just a lambda receiving an Int and returning a Boolean.
  4. Invoke whoAmI on the previous lambda.

Compile and run the previous code, getting:

I'm a typealias

This is because the definition of whoAmI above would be exactly the same as:

fun <T> ((T) -> Boolean).whoAmI() =
  println("I'm a typealias")

Now, add the following code to the same file, replacing the existing main function:

fun interface ISinglePredicate<T> { // 1

  fun accept(value: T): Boolean // 2

  fun other() { // 3
    println("I can have other methods")
  }
}

fun <T> ISinglePredicate<T>.whoAmI() = // 4
  println("I'm a functional interface")

fun main() {
  val isEvenFI = ISinglePredicate<Int> { number -> number % 2 == 0 } // 5
  isEvenFI.whoAmI() // 6
  isEvenFI.other() // 7
}

In this case, you:

  1. Create ISinglePredicate<T> as a functional interface.
  2. Define accept as the single abstract method for ISinglePredicate<T>.
  3. Also define other, proving that a functional interface must have one and only one abstract method, but it can also have other methods. A small note about these methods later.
  4. Create the same extension method, whoAmI, which prints the fact that this is about a functional interface.
  5. Create isEvenFI as an ISinglePredicate<Int> implementation using the handy syntax mentioned earlier, recalling the lambda expressions.
  6. Invoke whoAmI on isEvenFI.
  7. Also invoke other on the same isEvenFI.

When you run this code, you get:

I'm a functional interface
I can have other methods

This proves that a functional interface is conceptually very similar to a function type you define using typealias, but it:

  1. Allows you to define a new type.
  2. Is more type-safe because, like in the previous example, it allows you to define extension methods for a very specific type of function. The whoAmI on the type alias works for any function with one parameter returning Boolean. The whoAmI on ISinglePredicate<Int> works just on instances of the same explicit type.
  3. Allows you to define one and only one operation. The others can be methods working as default method implementations for an interface.
  4. Invokes the abstract method of the functional interface. To achieve this, you need to state its name explicitly. With the function you define using typealias, you just use () or invoke. With ISinglePredicate<T>, you need to call accept.

Note: In theory, an interface doesn’t define any methods but just operations, which are public and abstract by definition. When you read the method of the interface, it means methods adding something more to the one the interface defines through its operations. An example of this is:

interface Reader {
  fun readChar(): Char?
  fun readString(): String {
    TODO("Call readChar() until it returns null")
  }
}

Here, readChar is the operation and readString is the method.

Exercise 5.3: Can you implement Reader in the previous note using a function interface? How would you test it?

Interfaces with multiple operations or methods

Exercise 5.3 should make you think about the relationship between functional programming and the concept of interfaces with multiple operations. Why do you think interfaces with a single operation (SAM) are so important?

The answer is very simple, and it can be summarized with the concepts you find in the SOLID principles. You won’t revisit all of them here because it’s not the main topic, but just think about the meaning of the I there, which stands for the interface segregation principle.

This basically states that no clients should be forced to depend on methods they don’t use. Functional interfaces, or lambda expressions, define a single operation and make it impossible for clients to depend on the lambda expressions if they don’t use those functions.

At this point, you might argue that using just SAM would make it difficult to implement interfaces like Reader. That’s not the case at all — and you’ll prove it. :] Open Solid.kt and add the following code to define CharReader as the interface with the sole responsibility of providing a single Char at a time.

fun interface CharReader {
  fun readChar(): Char?
}

If you need to read a String, you can use the most fundamental principle of functional programming: composition. In the same file, add the following code:

fun interface StringReader {
  fun readString(charReader: CharReader): String
}

With this, you can implement StringReader by adding some code that’s basically the same as what you probably implemented in Exercise 5.3:

val stringReader = StringReader { charReader ->
  val result = StringBuilder()
  do {
    val nextChar = charReader.readChar()
    if (nextChar != null) {
      result.append(nextChar)
    }
  } while (nextChar != null)
  result.toString()
}

To make everything more functional, add:

fun String.toCharReader(): CharReader {
  var pos = 0
  return CharReader {
    if (pos < this.length) this[pos++] else null
  }
}

This is an extension function on String returning a CharReader you can use like this:

fun main() {
  println(stringReader.readString("This is String!".toCharReader()))
}

Run this code, and you get:

This is String!

In this single line of code, you:

  1. Create CharReader from String.
  2. Use stringReader to read String back from CharReader.
  3. Print String.

Thinking functionally, you managed to apply the interface segregation principle, splitting Reader into two different functional interfaces, CharReader and StringReader, reducing the dependency drastically.

Note: In Chapter 8, “Composition”, you’ll also learn to improve that code further. It has too many parentheses () and can be made more readable.

Returning a lambda expression as an output value

The return value of a higher-order function can also be a function. Open ReturnFunction.kt and write the following code:

fun countFrom(start: Int): () -> Int { // 1
  var count = start // 2
  return { // 3
    count++
  }
}

This function:

  1. Has an input parameter of type Int and returns a function of type () -> Int.
  2. Initializes count to the value of the start parameter.
  3. Returns a function that returns count and then increments its value.

You can test countFrom with the following code:

fun main() {
  val countFromFive = countFrom(5)
  println(countFromFive())
  println(countFromFive())
  println(countFromFive())
}

Run it, and you get:

5
6
7

As you see, countFrom initializes count, which is then captured by the lambda it returns. All the variables outside the scope of a lambda function that the lambda can access represent its closure.

As in the previous example where count is incremented, the lambda can access its closure and, if not constant, can also modify it.

Exercise 5.4: Implement an extension function isEqualsPredicate on the generic type T that returns a predicate that tests if a given value is equal to the same T. The signature should be the following:

fun <T> T.isEqualsPredicate(): (T) -> Boolean

How would the same function be different if you use the following functional interface?

fun interface SinglePredicate<T> {
  fun accept(other: T): Boolean  
}

Using lambda expressions as input and output

Functions with a lambda both as a return value and as an input parameter are the most interesting and fun (pun intended :]). You’ve actually seen many of these, and you’ll find many more ahead in the book.

As a simple example here, you can keep playing with Predicate. Open Predicates.kt and write the following code:

fun interface Predicate1<T> {
  fun accept(value: T): Boolean
}

Note: In this chapter, to make different examples coexist in the same project, sometimes you need to use a different name for the same concept. That’s why you append 1 to Predicate here. Of course, you wouldn’t do the same in your project.

This is a single functional interface for a predicate with a single parameter you’ve already met many times, such as during Exercise 5.4. Imagine, now, that you want to create a function and that accepts as input two Predicate1<T>s and returns another one, which is the logical and of the two. To be true, both predicates must be true.

You already know how to do this. In the same Predicates.kt file, add the following code:

infix fun <T> Predicate1<T>.and(
  other: Predicate1<T>
): Predicate1<T> = // 1
  Predicate1 { value ->
    this.accept(value) && other.accept(value) // 2
  }

In this code, you simply:

  1. Create an infix extension function and on Predicate1<T>.
  2. Return another Predicate1<T>, which evaluates the first one and, if true, other.

In the same way, define or like this:

infix fun <T> Predicate1<T>.or(
  other: Predicate1<T>
): Predicate1<T> =
  Predicate1 { value ->
    this.accept(value) || other.accept(value)
  }

You can use the same approach used in Exercise 5.4 with the following code:

fun <T> T.isEqualsPredicate1(): Predicate1<T> = // 1
  Predicate1 { value -> this == value }

fun <T> Iterable<T>.filterWithPredicate(predicate: Predicate1<T>) = // 2
  filter(predicate::accept)

fun main() {
  val predicate = 4.isEqualsPredicate1() or 5.isEqualsPredicate1()  //  3
  listOf(1, 2, 3, 4, 4, 5, 6, 7, 8, 8)
    .filterWithPredicate(predicate) // 4
    .forEach(::println)
}  

In this code, you:

  1. Define isEqualsPredicate1 using Predicate1<T>.
  2. Create filterWithPredicate on Iterable<T> for filtering values using Predicate1<T> instead of (T) -> Boolean.
  3. Use the or you created earlier to create Predicate1<T> for filtering values 4 and 5.
  4. Use filterWithPredicate with predicate.

Run this code, and you get:

4
4
5

This is really fun!

Exercise 5.5: Can you implement the same logic for implementing the example in the Imperative vs. declarative approach section using the definitions of Predicate1<T> and filterWithPredicate? Given a list of email addresses, you need to:

  • Filter the valid email addresses.
  • Filter the email addresses that are long enough.
  • Take the first five of them.

Give it a try and check the solution in Appendix E.

Higher-order functions and the inline keyword

In Chapter 3, “Functional Programming Concepts”, you learned about the convenience of using an inline keyword for lambda expressions or pure functions in general. When you just inline a pure function, IntelliJ warns you about the insignificant performance benefits. The same message also mentions that this isn’t the same if the lambda was passed as an input parameter to a higher-order function. What does the compiler mean by this?

Open Inline.kt and add the following code:

fun executor(fn: () -> Unit) {
  fn()
}

fun main() {
  executor { println("Hello World!") }
}

In this very simple code, you:

  1. Define executor, which just executes the lambda you pass as an input parameter.
  2. Invoke executor in main.

In this case, you don’t need to run this code, but repeat the same process you followed in Chapter 3, “Functional Programming Concepts” for the Kotlin decompiled code. When you do so, you end up with something like this:

public final class InlineKt {
   public static final void executor(@NotNull Function0 fn) { // 1
      Intrinsics.checkNotNullParameter(fn, "fn");
      fn.invoke();
   }

   public static final void main() {
      executor((Function0)null.INSTANCE); // 2
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }
}

As you see:

  1. executor becomes a Java static method with Function0 as an input parameter.
  2. main contains the simple invocation of executor.

This is what you’d normally expect. Now, just add the inline keyword to executor like this:

inline fun executor(fn: () -> Unit) { // HERE
  fn()
}

The first thing to note is that the IntelliJ warning you got in Chapter 3 doesn’t show up.

Figure 5.1: No warnings from IntelliJ
Figure 5.1: No warnings from IntelliJ

Check the decompiled code, and you see that main becomes:

   public static final void main() {
      int $i$f$executor = false;
      int var1 = false;
      String var2 = "Hello World!";
      boolean var3 = false;
      System.out.println(var2); // HERE
   }

You can see how main no longer has an executor invocation. There’s not even the invocation of the fn you pass as a parameter. The Kotlin compiler has copied the content of fn exactly where the previous executor invocation was.

The inlined executor is very simple, but if it contained more code, the resultant bytecode would also need much more space. On the other hand, you could return to the warning situation using the noinline keyword like this:

inline fun executor(noinline fn: () -> Unit) { // HERE
  fn()
}

In this case, the Kotlin compiler would complain again, like this:

Figure 5.2: Using the noinline keyword
Figure 5.2: Using the noinline keyword

This is because the decompiled code would be like the initial one.

Non-local returns

Inlining can be useful for another reason, though. Update the code in Inline.kt like the following:

fun executor(fn: () -> Unit) {
  fn()
}

fun main() {
  executor {
    var count = 0
    while (true) {
      count++
      if (count > 5) {
        return // ERROR
      }
    }
  }
  println("The End!")
}

If you don’t inline executor, the Kotlin compiler will complain because it doesn’t know from what context it should actually return.

Figure 5.3: Undefined return
Figure 5.3: Undefined return

Here, you have two options:

  1. Use the label return@executor specifying that you want to exit from executor.
  2. Inline executor.

In the first case, you can write:

fun main() {
  executor {
    var count = 0
    while (true) {
      count++
      if (count > 5) {
        return@executor
      }
    }
  }
  println("The End!")
}

In the second case, you can just inline executor like this:

inline fun executor(fn: () -> Unit) {
  fn()
}

You must be very careful, though, because the behavior of your program differs in the two cases.

When you use return@executor, you’re saying that when you reach return, you’re exiting from the body of executor. Run main, and you get:

The End!

If you inline executor, you’re copying return in main, and reaching it means exiting from main. If you run the code, you get nothing because the println statement won’t be reached.

The crossinline keyword

For completeness’ sake, it’s useful to look at the following code:

fun executor(fn: () -> Unit) { // 1
  fn()
}

inline fun executorExecutor(fn: () -> Unit) { // 2
  executor {
    fn() // ERROR
  }
}

Here, you note that:

  1. executor isn’t inlined.
  2. executorExecutor is inlined and invokes the fn it receives as an input parameter using executor.

As you can see, the Kotlin compiler is complaining:

Figure 5.4: Can't inline 'fn' here; it may contain non-local returns
Figure 5.4: Can't inline 'fn' here; it may contain non-local returns

The message in Figure 5.4 explains everything.

The reason is that the compiler doesn’t know how to handle fn non-local returns and suggests a solution of introducing the crossinline keyword to the parameter like this:

inline fun executorExecutor(crossinline fn: () -> Unit) {
  executor {
    fn()
  }
}

This informs the Kotlin compiler that the lambda you pass as an input parameter of executorExecutor can’t contain any non-local return. If it does, you get an error:

Figure 5.5: 'return' is not allowed here
Figure 5.5: 'return' is not allowed here

If you really want to allow return in fn, you can simply use inline for both executor and executorExecutor:

inline fun executor(fn: () -> Unit) { // HERE
  fn()
}

inline fun executorExecutor(fn: () -> Unit) { // HERE
  executor {
    fn()
  }
}

Challenges

In this chapter, you learned a lot about higher-order functions that are basically functions of functions. You’ve already done some interesting exercises, but now it’s time for a few more challenges.

Challenge 5.1: Mapping is important

In this chapter, you learned how to implement different types of higher-order functions, and in the next chapter, you’ll see many more. A very important one is called map. This is a function that applies a given function fn of type (A) -> B to all the elements of a collection of items of type A, getting a collection of items of type B.

Can you implement the function map for the Array<A> type with the following signature?

fun <A, B> Array<A>.map(fn: (A) -> B): Array<B>

When you run this code:

fun main() {
  val square = { a: Int -> a * a }
  val toString = { a: Int -> "This is $a" }
  arrayOf(1, 2, 3)
    .map(square)
    .forEach(::println)
  arrayOf(1, 2, 3)
    .map(toString)
    .forEach(::println)
}

You should get:

1
4
9
This is 1
This is 2
This is 3

Challenge 5.2: Prime number filtering

Write a higher-order function all returning a new array containing all the values in an Array<T> for which a given Predicate1<T> is true. You can find Predicate1<T>’s definition in Predicates.kt.

fun <T> Array<T>.all(predicate: Predicate1<T>) : Array<T>

Then, use it to return all the positive prime values in Array<Int>. A number is prime if it’s not evenly divisible with any number other than 1 and itself.

Key points

  • A lambda expression is a function you can use as a value.
  • A higher-order function accepts other functions as input parameters or provides other functions as return values.
  • The declarative approach makes code more readable.
  • Kotlin allows you to define functional interfaces, which are a handy way to define interfaces with a single abstract method that can be used as lambda expressions.
  • Functional interfaces are also called SAM, which stands for single abstract method.
  • A closure of a function is the set of all the variables the function can access, which have been defined outside its own body.
  • A lambda function can modify its non-constant closures.
  • Always consider using the inline keyword for higher-order functions, but double-check that the behavior is what you expect, especially in the context of non-local returns.
  • The noinline and crossinline keywords are two additional options you have to control the flow of your program when using higher-order functions.

Where to go from here?

Congratulations! In this chapter, you learned a lot about what higher-order functions are and how they can help you write your code in a more declarative way. Composition and higher-order functions are the essentials of functional programming. Right now, you’re just at the beginning. In the next chapter, you’ll be introduced to immutability and recursion: two important topics when using higher-order functions.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.