Examples of Functional Kotlin

A lot of higher-order functions are available for lists and arrays, though some are available on other types, including one that we will see later in this chapter. We get lists all the time, from database queries to Web service calls. Kotlin’s higher-order functions make it easy to manipulate those lists.

For those of you with RxJava experience, a lot of these will look familiar. RxJava attempts to implement a functional API on top of Java for processing streams of data. RxJava operators are akin to higher-order functions, particularly when you use them on Java 8+ and can use lambda expressions. However, while RxJava is focused on processing streams asynchronously, Kotlin does not address threading directly with its higher-order functions.

We covered some of these back in the chapter on collections.

first() / firstOrNull()

Kotlin offers a really simple first() function that returns the first element from the list:

data class Event(val id: Int)

fun main() {
  val events = listOf(Event(1), Event(5), Event(1337), Event(24601), Event(42), Event(-6))

  println(events.first())
}

However, the more powerful form of first() takes a lambda expression and returns the first element that matches, as we saw above:

data class Event(val id: Int)

fun main() {
  val events = listOf(Event(1), Event(5), Event(1337), Event(24601), Event(42), Event(-6))

  val leetEvent = events.first { it.id == 1337 }

  println(leetEvent)
}

first() will throw a NoSuchElementException, though, if there is no matching element. So unless you are in position to catch that exception or know for certain that there will always be a match, first() is annoying.

firstOrNull() works the same as first(), but it returns null if there is no match:

data class Event(val id: Int)

fun main() {
  val events = listOf(Event(1), Event(5), Event(1337), Event(24601), Event(42), Event(-6))

  val noEvent = events.firstOrNull { it.id == 2343 }

  println(noEvent)
}

With the Elvis operator, this also makes it easy for you to implement a first-or-default pattern:

data class Event(val id: Int)

fun main() {
  val events = listOf(Event(1), Event(5), Event(1337), Event(24601), Event(42), Event(-6))

  val oneEvent = events.firstOrNull { it.id == 2343 } ?: Event(2343)

  println(oneEvent)
}

filter()

Whereas first() and firstOrNull() only give you one element (at most), filter() gives you another list or array with the subset matching some business rule (i.e., where a lambda expression returns true), as we saw back in the chapter on collections:

  val things = listOf("foo", "bar", "goo")

  things.filter { it[1]=='o' }.forEach { println(it) }

Higher-order functions are designed to be chained together, so you can use filter() and firstOrNull() to find the first event with a negative even ID:

data class Event(val id: Int)

fun main() {
  val events = listOf(Event(1), Event(5), Event(1337), Event(24601), Event(42), Event(-6))

  val evenNegativeEvent = events.filter { it.id % 2 == 0 }.firstOrNull { it.id < 0 }

  println(evenNegativeEvent)
}

Here, our filter() lambda expression returns those whose IDs are even (the modulo of 2 equals 0). Our first() lambda expression then finds the first of those where the ID is negative.

In this case, firstOrNull() could just implement both rules, though, using the logical-and operator (&&):

data class Event(val id: Int)

val events = listOf(Event(1), Event(5), Event(1337), Event(24601), Event(42), Event(-6))

val evenNegativeEvent = events.firstOrNull { it.id % 2 == 0 && it.id < 0 }

println(evenNegativeEvent)

map()

Sometimes, we have data in one form that we need to convert into another form. For that, there is the map() operator, as we saw in the chapter on collections:

  val things = listOf("foo", "bar", "goo")

  things
    .map { it.toUpperCase() }
    .forEach { println(it) }

map() takes a lambda expression, passes each element in the stream to that lambda expression, and collects the objects returned by the lambda expression.

any()

Sometimes, we want to run an algorithm on a collection and get some single result back. For example, any() returns true if any of the elements in the collection result in a true value being returned from a supplied lambda expression:

data class Event(val id: Int)

fun main() {
  val events = listOf(Event(1), Event(5), Event(1337), Event(24601), Event(42), Event(-6))

  println(events.any { it.id < 0 })
  println(events.any { it.id > 100000 })
}

Here, the first any() returns true, because one of the elements does have a negative ID value. The second any() returns false, because none of the elements has an ID above 100000.

Note that any() only tests elements until it gets a true result (or reaches the end, whichever comes first):

data class Event(val id: Int)

fun main() {
  val events = listOf(Event(1), Event(5), Event(1337), Event(24601), Event(42), Event(-6))

  println(events.any {
    println(it)

    it.id > 10
  })
}

Here, we introduce a side-effect into our lambda expression: printing the current item being examined by any(). Running this gives us:

Event(id=1)
Event(id=5)
Event(id=1337)
true

Once any() gets a true value, it stops and returns true, so we do not wind up with printed output of the other events in the list.

fold()

These sorts of higher-order functions do not have to be as simple as the ones shown above. They can be as complex as is necessary, and that in turn might require more complex lambda expressions.

For example, sometimes you want to perform a calculation on each of the elements in a collection and generate a single result from those. For that, we have fold(). fold() takes two parameters:

The first invocation of the lambda expression gets the initial value as the accumulator value. The second invocation gets the result of the first invocation as the accumulator value. And so on through the collection:

data class Event(val id: Int)

fun main() {
  val events = listOf(Event(1), Event(5), Event(1337), Event(24601), Event(42), Event(-6))

  val joined = events.fold("") { str, event ->
    if (str.length==0) event.toString() else "$str,$event"
  }

  println(joined)
}

Each time our lambda expression gets invoked, it gets the concatenated results so far plus the next event in the list. Our lambda expression sees if the concatenated results are empty, and if so just returns a String representation of the first event. Otherwise, it adds a comma and the String representation of the current event. As a result, that accumulated String keeps growing:

Invocation str Value event Value
1 "" Event(1)
2 "Event(1)" Event(5)
3 "Event(1),Event(5)" Event(1337)
4 "Event(1),Event(5),Event(1337)" Event(24601)
5 "Event(1),Event(5),Event(1337),Event(24601)" Event(42)
6 "Event(1),Event(5),Event(1337),Event(24601),Event(42)" Event(-6)

When fold() reaches the end of the collection, it returns whatever the last lambda expression returned.

Of course, Kotlin offers a joinToString() function that would handle this for us more simply:

data class Event(val id: Int)

fun main() {
  val events = listOf(Event(1), Event(5), Event(1337), Event(24601), Event(42), Event(-6))

  val joined = events.joinToString(",")

  println(joined)
}

Prev Table of Contents Next

This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.