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:
- Filters invalid email addresses.
- Uses only email addresses with more than 10 characters.
- 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:
- Define
imperative
as a function that returns the first five valid email addresses longer than 10 characters. - Initialize the
List
you’ll return as the result. - Iterate over all the email addresses in input.
- 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. - 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:
- Define
declarative
with the same signature asimperative
. - Use
filter
to remove the invalid email addresses. - Use
filter
again to remove email addresses shorter than or equal to 10 characters. - Take the first
5
email addresses usingtake
.
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:
- Define
isEmailValid
, which is a pure function that checks if the input email address is valid. - Create
isEmailLongEnough
to check the length of the email address. - Use the reference
::isEmailValid
as the parameter of the firstfilter
. - Use
::isEmailLongEnough
as the parameter of the secondfilter
.
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:
- It’s an extension function for
Iterable<T>
. - 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 aBoolean
. In this case, the predicate is a lambda accepting an object of typeT
as input, which is the type parameter forIterable<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 theInt
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 theInt
value you invoketimes
on. In the case of10.times{}
, the receiver is10
. - The range as
1..this
, and you iterate over it usingforEach
to executefn
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 ofIterable<T>
for which a predicate you provide in input evaluates totrue
. Remember thatIterable<T>
is the abstraction of all the collections providingIterator<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 implementfirst
onArray<T>
?The current implementation of
first
throws an exception if the collection is empty, so there’s no firstT
. How would you implement the functionfirstOrNull
onArray<T>
returningnull
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 Int
s. 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 oneT
is greater than anotherT
.
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:
- Define
bubbleSort
as a generic function with the type variableT
. - Pass the strategy as a lambda function of type
(T, T) -> Boolean
, which returnstrue
if the first value is greater than the second. - Use the
isLarger
lambda expression to compare each pair of values in the array. - 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
andCommandExecutor
.Command
abstracts every possible operation thatCommandExecutor
can run. In other words, aCommand
represents a task and you can pass aCommand
to aCommandExecutor
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:
- Use the
fun
keyword as a qualifier for theIsLarger<T>
interface. - 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:
- Create
bubbleSortFI
as a new version of the previousbubbleSort
. - Define the strategy as a parameter of type
IsLarger<T>
. - Invoke
isLarger
, which is the operation you defined inIsLarger<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:
- Give the name
SinglePredicate<T>
to any function receiving a single parameter of typeT
and returning aBoolean
. - Define
whoAmI
as an extension function forSinglePredicate<T>
, which just prints a message saying that the receiver type istypealias
. - Define the
isEven
lambda, which returnstrue
if anInt
value you pass as a parameter is even. Note how you’re just creating a lambda with no relation to the nameSinglePredicate<T>
you defined earlier. It’s just a lambda receiving anInt
and returning aBoolean
. - 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:
- Create
ISinglePredicate<T>
as a functional interface. - Define
accept
as the single abstract method forISinglePredicate<T>
. - 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. - Create the same extension method,
whoAmI
, which prints the fact that this is about a functional interface. - Create
isEvenFI
as anISinglePredicate<Int>
implementation using the handy syntax mentioned earlier, recalling the lambda expressions. - Invoke
whoAmI
onisEvenFI
. - Also invoke
other
on the sameisEvenFI
.
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:
- Allows you to define a new type.
- 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 returningBoolean
. ThewhoAmI
onISinglePredicate<Int>
works just on instances of the same explicit type. - Allows you to define one and only one operation. The others can be methods working as default method implementations for an interface.
- 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()
orinvoke
. WithISinglePredicate<T>
, you need to callaccept
.
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 andreadString
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:
- Create
CharReader
fromString
. - Use
stringReader
to readString
back fromCharReader
. - 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:
- Has an input parameter of type
Int
and returns a function of type() -> Int
. - Initializes
count
to the value of thestart
parameter. - 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 typeT
that returns a predicate that tests if a given value is equal to the sameT
. 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
toPredicate
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:
- Create an infix extension function
and
onPredicate1<T>
. - 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:
- Define
isEqualsPredicate1
usingPredicate1<T>
. - Create
filterWithPredicate
onIterable<T>
for filtering values usingPredicate1<T>
instead of(T) -> Boolean
. - Use the
or
you created earlier to createPredicate1<T>
for filtering values4
and5
. - Use
filterWithPredicate
withpredicate
.
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>
andfilterWithPredicate
? 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:
- Define
executor
, which just executes the lambda you pass as an input parameter. - Invoke
executor
inmain
.
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:
-
executor
becomes a Java static method withFunction0
as an input parameter. -
main
contains the simple invocation ofexecutor
.
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.
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:
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
.
Here, you have two options:
- Use the label
return@executor
specifying that you want to exit fromexecutor
. - 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:
-
executor
isn’t inlined. -
executorExecutor
is inlined and invokes thefn
it receives as an input parameter usingexecutor
.
As you can see, the Kotlin compiler is complaining:
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:
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
andcrossinline
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.