Skip to content

7. Functional Types, Lambdas, and Events

In many programs, we need to pass not data but behavior to a function: the sorting criterion, the filtering condition, or the action to be performed when something happens. Kotlin addresses this need by treating functions as values just like any other: a function has a type—the functional type, denoted, for example, as [(Int, Int) -> Int]; it can be stored in a variable, passed as a parameter, returned as the result of another function, and executed whenever desired. A function that takes or returns a function is called a higher-order function.

Java, as a reminder, does not have functional types: it relies on functional interfaces—interfaces with a single abstract method ([Runnable], [Comparator], [Function<T, R>]…), and it wasn’t until Java 8 (2014) that it allowed them to be implemented using lambda expressions and method references. Kotlin has supported lambdas since its very first version, and it can also work with Java’s functional interfaces, since it directly uses Java libraries. We’ve already used lambdas extensively without going into detail, in the collection operations covered in Chapter 5: in [persons.filter { it.age >= 18 }], we pass a lambda to the function [filter]. This chapter explains what lies behind this syntax. It concludes with events, which Kotlin—like Java—handles using “listeners”: functions, most often lambdas, that register with an emitter to be notified of what is happening there. Graphical user interfaces (Swing, JavaFX, Compose) and web frameworks alike rely on this mechanism: a button click or the arrival of a HTTP request is signaled to registered listeners. This course does not cover graphical user interfaces; all examples are console programs.

The programs in this chapter are organized in the Gradle project [07-lambdas], with one package per program: [src/main/kotlin/lambdas01/Lambdas01.kt], [src/main/kotlin/lambdas02/Lambdas02.kt]… They are run using [gradlew -q run -Pmain=lambdas01.Lambdas01Kt] or by clicking the green triangle in IntelliJ or IDEA. The project requires no libraries other than the standard Kotlin library: functional types, lambdas, and scope functions are included, and the interfaces in the [java.util.function] package are the same as those in JDK.

7.1. Functional types and predefined functional interfaces

7.1.1. The Concept of a Functional Type

A functional type describes a model (also called a signature or a form) of a function: the types of its parameters, enclosed in parentheses, followed by an arrow, and then the type of its return value. A variable of a functional type can refer to any function of this form and execute it. Here are a few examples:

  • [() -> Unit]: a function with no parameters that returns no useful result ([Unit] plays the role of Java’s [void]);
  • [() -> String]: a function with no parameters that returns a string;
  • [(Int) -> Boolean]: a function that takes an integer and returns a boolean, in other words, a predicate;
  • [(Int, Int) -> Int]: a function that takes two integers and returns an integer;
  • [(String) -> (Int) -> Int]: a function that takes a string and returns… a function of type [(Int) -> Int] (the arrow is read from right to left);
  • [(() -> Unit)?]: a function that may be absent; the parentheses are necessary because [() -> Unit?] would otherwise denote a function returning a [Unit?]. Here we see the safety property with respect to [null] from Chapter 3: such a function is denoted by [f?.invoke()];
  • [String.(Int) -> String]: a function with a receiver; it is applied to a string—which its code denotes as [this]—as an extension function (Chapter 4). It can be called as an extension (["ab".f(3)]) or as an ordinary function whose receiver is the first parameter ([f("ab", 3)]).

Parameters can be named to document the type: [(n1: Int, n2: Int) -> Int] is the same type as [(Int, Int) -> Int].

There are three ways to obtain a value of a function type:

  • write a reference to an existing function that has the correct form: [val task: () -> Unit = ::display];
  • Write a lambda expression: [val task: () -> Unit = { println("task") }];
  • write an anonymous function: [val task = fun() { println("task") }], or an object of a class that implements the functional type (a functional type is an interface, as we’ll see). These two notations are less common.

The function is then executed like an ordinary function: [task()]. This is a shorthand notation for calling the method [invoke] of the function object: [task.invoke()]. This is the first noticeable difference from Java, where you must call the interface’s method by name ([run], [apply], [test]…); Kotlin behaves here like C#, where a delegate is called like a method.

7.1.2. Kotlin Functional Types and java.util.function Interfaces

Before Java 8, each Java library defined its own single-method interfaces ([Runnable], [Comparator], [ActionListener]…). Java 8 introduced the [java.util.function] package, which provides 43 generic functional interfaces. Kotlin does not need them: a functional type is written directly. However, these interfaces can still be used from Kotlin, and you’ll encounter them whenever you call a Java library. The following table maps the main Java interfaces to Kotlin’s functional types:

Java Interface
Kotlin Type
Functions Represented
[Runnable]: [void run()]
[() -> Unit]
No parameters, no results
[Supplier<T>] : [T get()]
[() -> T]
No parameters, one result: one “supplier”
[Consumer<T>]: [void accept(T t)]
[(T) -> Unit]
one parameter, no result: a “consumer”
[BiConsumer<T, U>]
[(T, U) -> Unit]
two parameters, no results
[Function<T, R>]: [R apply(T t)]
[(T) -> R]
one parameter, one result
[BiFunction<T, U, R>]
[(T, U) -> R]
two parameters, one result
[UnaryOperator<T>]
[(T) -> T]
Parameter and result of the same type
[BinaryOperator<T>]
[(T, T) -> T]
Two parameters and one result of the same type
[Predicate<T>]: [boolean test(T t)]
[(T) -> Boolean]
one parameter, a Boolean result: a “predicate”
[BiPredicate<T, U>]
[(T, U) -> Boolean]
two parameters, a Boolean result
[Comparator<T>]: [int compare(T o1, T o2)]
[(T, T) -> Int]
two parameters of the same type, an integer result
[Callable<V>] : [V call()]
[() -> V]
such as [Supplier<V>]; in Kotlin, no checked exception needs to be declared
[IntBinaryOperator], [IntPredicate]…
[(Int, Int) -> Int], [(Int) -> Boolean]…
The Java versions for primitive types

In Kotlin, there is no practical limit to the number of parameters a function type can have: [(Int, Int, Int) -> Int] is written just as simply as the others, whereas in Java, since there is no predefined interface with three parameters, you have to define your own.

How does the Java Virtual Machine, which only recognizes classes and interfaces, represent a functional type? Through generic interfaces from the Kotlin library: [kotlin.jvm.functions.Function0<R>], [Function1<P1, R>], [Function2<P1, P2, R>]… all the way up to [Function22], followed by an interface [FunctionN] beyond that. Each has a single method, [invoke]: [(Int, Int) -> Int] is compiled into [Function2<Integer, Integer, Integer>], whose method is [operator fun invoke(p1: Int, p2: Int): Int]. It is the modifier [operator] of [invoke] (see operator overloading in Chapter 4) that allows the notation [f(2, 3)]. A functional type is therefore, for JVM, a functional interface like those in Java. Since Java generics apply only to object types, [Int] values passed to a [(Int, Int) -> Int] are converted to [Integer] and vice versa (boxing and unboxing). Java avoids these conversions with its specialized interfaces ([IntBinaryOperator]…); Kotlin avoids them in a different way, using the [inline] functions, which we’ll discuss later.

The program [Lambdas01.kt] uses these types with function references:

package lambdas01

import java.util.function.IntBinaryOperator
import java.util.function.Predicate
import java.util.function.Supplier

// Kotlin function types and Java functional interfaces

fun main() {
    // 1 - () -> Unit: no parameter, no result
    val task: () -> Unit = ::display
    // execution: the function is called like an ordinary function
    task()
    // which is shorthand for calling its invoke method
    task.invoke()

    // 2 - (Int, Int) -> Int: two parameters, one result
    val operation: (Int, Int) -> Int = ::add
    println("operation(2, 3)=${operation(2, 3)}")
    // () -> String: no parameter, one result
    val greet: () -> String = ::greet
    println("greet()=${greet()}")

    // 3 - (String) -> Unit: one parameter, no result
    val write: (String) -> Unit = ::println
    write("(String) -> Unit executed println")

    // 4 - (Int) -> Boolean: a predicate
    val isEven: (Int) -> Boolean = ::isEven
    println("isEven(8)=${isEven(8)}")
    val numbers = mutableListOf(3, 8, 1, 6, 5, 4)
    // removeAll from the Kotlin library expects an (Int) -> Boolean
    val modified = numbers.removeAll(isEven)
    println("even numbers removed: $modified, remaining: $numbers")

    // 5 - a Comparator<T> (Java interface) built from a function (SAM conversion)
    val names = mutableListOf("Paul", "Mélanie", "Jo", "Sébastien")
    names.sortWith(Comparator(::compareLengths))
    println("sorted by length: $names")

    // 6 - the java.util.function interfaces remain usable
    val supplier: Supplier<String> = Supplier(::greet)
    val multiply = IntBinaryOperator(::multiply)
    val predicate = Predicate<Int> { it > 4 }
    println("supplier.get()=${supplier.get()}")
    println("multiply.applyAsInt(2, 3)=${multiply.applyAsInt(2, 3)}")
    // removeIf is a Java method (Collection): it expects a Predicate
    val values = mutableListOf(10, 3, 8, 7, 2, 1)
    values.removeIf(predicate)
    // a Kotlin function is automatically converted to a Predicate (SAM conversion)
    values.removeIf(isEven)
    values.removeIf { it == 1 }
    println("after removeIf: $values")

    // 7 - a nullable function type: the function may be absent
    execute(null)
    execute(task)

    // 8 - a function type with receiver: String.(Int) -> String
    val times: String.(Int) -> String = String::repeat
    println("\"ab\".times(3)=${"ab".times(3)}, times(\"cd\", 2)=${times("cd", 2)}")

    // 9 - a function is an object, an instance of a class that implements an interface
    println("class: ${operation::class.java.name}")
    println("interfaces: ${operation::class.java.interfaces.contentToString()}")
    val sum: (Int, Int) -> Int = { n1, n2 -> n1 + n2 }
    println("class of the lambda: ${sum::class.java.name}")
    println("interfaces: ${sum::class.java.interfaces.contentToString()}")
}

// a function that receives a possibly absent function
fun execute(callback: (() -> Unit)?) {
    // safe call: nothing is executed if callback is null
    callback?.invoke()
    println("callback ${if (callback == null) "absent" else "present"}")
}

// the referenced functions: top-level functions of the file
fun display() {
    println("display() executed by task()")
}

fun add(n1: Int, n2: Int): Int = n1 + n2

fun multiply(n1: Int, n2: Int): Int = n1 * n2

fun greet(): String = "Hello"

fun isEven(n: Int): Boolean = n % 2 == 0

fun compareLengths(s1: String, s2: String): Int = s1.length.compareTo(s2.length)

Results:

> gradlew -q run -Pmain=lambdas01.Lambdas01Kt
display() executed by task()
display() executed by task()
operation(2, 3)=5
greet()=Hello
(String) -> Unit executed println
isEven(8)=true
even numbers removed: true, remaining: [3, 1, 5]
sorted by length: [Jo, Paul, Mélanie, Sébastien]
supplier.get()=Hello
multiply.applyAsInt(2, 3)=6
after removeIf: [3]
callback absent
display() executed by task()
callback present
"ab".times(3)=ababab, times("cd", 2)=cdcd
class: lambdas01.Lambdas01Kt$main$operation$1
interfaces: [interface kotlin.jvm.functions.Function2]
class of the lambda: lambdas01.Lambdas01Kt$$Lambda/0x000000001f04a7f0
interfaces: [interface kotlin.jvm.functions.Function2]

Comments:

  • line 11: the variable [task] is of type [() -> Unit]. It is assigned a function reference: [::display] refers to the top-level function [display] declared later in the file (line 79). The compiler verifies that it has the required form (no parameters, no return value) and creates the function object that calls it. In Java, an auxiliary class would have been needed to contain these methods, since the implicit class of a compact source file cannot be named; in Kotlin, top-level functions are referenced directly;
  • line 13: the function is executed by calling it as an ordinary function. The function [display] is executed (line 1 of the results). Line 15: the same result is obtained by explicitly calling the method [invoke] (line 2 of the results);
  • Line 18: [(Int, Int) -> Int] refers to a function with two parameters, [Int], and returning an [Int]. The function [add] (line 83) is appropriate. The function is called via [operation(2, 3)] (line 19);
  • line 21: a parameterless function returning a string, the equivalent of Java’s [Supplier<String>]. Note that the local variable [greet] has the same name as the function [greet]: in [::greet], this refers to the function, and in [greet()], it refers to the local variable, which masks the function;
  • Line 25: a function with one parameter, [String], that returns nothing—the equivalent of Java’s [Consumer<String>]. The standard library function [println] is overloaded ([println(Int)], [println(Any?)]…): it is the expected type, [(String) -> Unit], that allows the compiler to choose the correct overload;
  • Line 29: a predicate, equivalent to [Predicate<Integer>]. Line 33: the Kotlin library’s extension function [removeAll] specifically expects a parameter of type [(T) -> Boolean]: it removes from the mutable list all elements for which the predicate evaluates to [true], and returns [true] if it has removed at least one element (line 7 of the results);
  • Line 38: The function [sortWith] expects a [Comparator<T>]. In Kotlin, [Comparator] is an alias for the Java interface [java.util.Comparator]. [Comparator(::compareLengths)] creates a [Comparator<String>] from the function [compareLengths]: this is a SAM conversion (“Single Abstract Method”), which creates the object implementing the Java interface from a Kotlin function of the same form. The function [compareLengths] (line 91) uses [compareTo], which compares two [Int] objects without the risk of capacity overflow (which a simple subtraction does not guarantee);
  • lines 42–44: the interfaces of *[java.util.function] can be used as-is. They are constructed in the same way, via an explicit SAM conversion: the interface name followed by a function reference or a lambda. They are then executed via their Java method ([get], [applyAsInt]*, [test]…), just as in Java;
  • lines 49–52: [removeIf] is a Java method of the [Collection] interface; it expects a [Predicate]. You can pass it a *[Predicate] (line 49), but also directly a value of type *[(Int) -> Boolean] (line 51) or a lambda (line 52): When calling a Java method that expects a functional interface, Kotlin automatically performs the SAM conversion. This is what makes Java libraries just as pleasant to use from Kotlin as they are from Java. The initial list (10, 3, 8, 7, 2, 1) successively removes elements greater than 4, even elements, and then the element 1 (line 11 of the results);
  • lines 56–57 and 72–75: the function *[execute] is passed a function that may be absent, of type *[(() -> Unit)?]. The safe call [callback?.invoke()] does nothing if [callback] is equal to [null]. You cannot write [callback?()]: the safe call to a nullable function goes through [invoke]. This will be the standard notation for optional listeners;
  • Line 60: a functional type with a receiver. The reference [String::repeat] refers to the method [repeat] of the class [String], which applies to a string and takes an integer. Line 61 is referred to in two ways: as an extension, ["ab".times(3)], or as a function with two parameters, [times("cd", 2)]. We’ll see functions with receivers again in the scope functions [apply] and [with];
  • lines 64–68: a function is an object. The class of the function reference [operation] (line 16 of the output) is a class generated by the Kotlin compiler, named after the location where the reference appears; the class of the lambda [sum] (line 18 of the results) is generated at runtime by the virtual machine, just like Java lambdas: its name contains [$$Lambda] followed by an address that changes from one execution to the next. In both cases, the class implements the [kotlin.jvm.functions.Function2] interface (lines 17 and 19 of the results). A Kotlin function is therefore not a new type of object: it is an instance of an ordinary class that implements a functional interface.

7.2. Defining Your Own Functional Types

Functional types are often sufficient, but you may want to give a name to a function model. Kotlin offers two tools for this purpose, which are very different in nature.

The first is a type alias, declared using the keyword [typealias]:

  • [typealias BinaryOperation = (n1: Int, n2: Int) -> Int]

An alias does not create a new type: [BinaryOperation] is another name for [(Int, Int) -> Int], and the two notations are interchangeable. The alias makes the code more readable when the functional type is long or appears frequently.

The second is the functional interface, declared by [fun interface]:

  • [fun interface Operation { fun calculate(n1: Int, n2: Int): Int }]

This is the exact equivalent of a functional Java interface: an interface with only one abstract method (also known as a SAM, or “Single Abstract Method” interface). It may also have methods with bodies and non-abstract properties; these do not count. The keyword [fun] preceding [interface] serves the same purpose as the Java annotation [@FunctionalInterface], with one important difference: it is not optional. It is this keyword that enables the SAM conversion—that is, the creation of an [Operation] object from a lambda or a function reference: [Operation { n1, n2 -> n1 + n2 }]. Without the keyword [fun], a Kotlin interface—even one reduced to a single method—can only be implemented by a class or an anonymous object ([object : Operation { … }]).

Compared to the type [(Int, Int) -> Int], the type [Operation] is a truly new type, distinct from all others: it has a name that describes its purpose, its method has a descriptive name ([calculate] rather than [invoke]), it can have additional methods, and it can be implemented by a named class. Conversely, it is executed by its method name ([op.calculate(2, 3)]), not by [op(2, 3)].

A variable named [op] of type [Operation], like a variable of type [(Int, Int) -> Int], references only one function at a time. Unlike C#, which offers “multicast delegates,” neither Java nor Kotlin provides a way to store multiple functions in a single variable and execute them all with a single call: when needed, we store the functions in a list and execute them one by one. This is exactly what we’ll do with event listeners.

Consider the example [Lambdas02.kt]:

package lambdas02

import java.util.function.IntBinaryOperator

// defining your own function types: fun interface and typealias

fun main() {
    // a variable of type Operation receives the increase function of the companion object
    var op = Operation(Calculations::increase)
    // the operation is executed
    var n = op.calculate(4, 7)
    println("n=$n")

    // creation of an object c1 of type Calculations
    val c1 = Calculations()
    // op is given the add method of the object c1
    op = Operation(c1::add)
    n = op.calculate(2, 3)
    println("n=$n")
    // then the subtract method of c1
    op = Operation(c1::subtract)
    n = op.calculate(2, 3)
    println("n=$n")

    // a variable references only one function: to execute several, a list is used
    val operations = listOf(
        Operation(c1::add),
        Operation(c1::subtract),
        Operation { n1, n2 -> n1 * n2 },
    )
    for (operation in operations) {
        println("-> ${operation.calculate(10, 4)}")
    }

    // a method of the interface builds a new operation
    val squaredSum = Operation(c1::add).then { x -> x * x }
    println("squaredSum.calculate(1, 2)=${squaredSum.calculate(1, 2)}")

    // Operation and IntBinaryOperator have the same shape but are two different types
    val other = IntBinaryOperator(op::calculate)
    println("other.applyAsInt(5, 1)=${other.applyAsInt(5, 1)}")

    // a generic functional interface
    val length = Transformation(String::length)
    val stars = Transformation("*"::repeat)
    println("length(\"Hello\")=${length.transform("Hello")}")
    println("stars(5)=${stars.transform(5)}")

    // a type alias: simply another name for (Int, Int) -> Int
    val f: BinaryOperation = { n1, n2 -> n1 - n2 }
    // BinaryOperation and (Int, Int) -> Int are the same type
    val g: (Int, Int) -> Int = f
    // but Operation is a different type: a conversion is needed
    val op2 = Operation(g)
    println("f(5, 1)=${f(5, 1)}, g(5, 1)=${g(5, 1)}, op2.calculate(5, 1)=${op2.calculate(5, 1)}")
}

// definition of a functional interface: a single abstract method
// taking two integers as parameters and returning an integer
fun interface Operation {
    fun calculate(n1: Int, n2: Int): Int

    // a method with a body does not count: the interface remains functional
    fun then(next: (Int) -> Int): Operation = Operation { n1, n2 -> next(calculate(n1, n2)) }
}

// a generic functional interface: T -> R
fun interface Transformation<T, R> {
    fun transform(value: T): R
}

// a type alias: no new type is created
typealias BinaryOperation = (n1: Int, n2: Int) -> Int

// a class whose methods have the shape of the Operation interface
class Calculations {
    // two instance methods
    fun add(n1: Int, n2: Int): Int {
        println("add($n1,$n2)")
        return n1 + n2
    }

    fun subtract(n1: Int, n2: Int): Int {
        println("subtract($n1,$n2)")
        return n1 - n2
    }

    // a method of the companion object: the equivalent of a static method
    companion object {
        fun increase(n1: Int, n2: Int): Int {
            println("increase($n1,$n2)")
            return n1 + 2 * n2
        }
    }
}

Results:

> gradlew -q run -Pmain=lambdas02.Lambdas02Kt
increase(4,7)
n=18
add(2,3)
n=5
subtract(2,3)
n=-1
add(10,4)
-> 14
subtract(10,4)
-> 6
-> 40
add(1,2)
squaredSum.calculate(1, 2)=9
subtract(5,1)
other.applyAsInt(5, 1)=4
length("Hello")=5
stars(5)=*****
f(5, 1)=4, g(5, 1)=4, op2.calculate(5, 1)=4

Comments:

  • lines 60–64: definition of the functional interface [Operation]. In Kotlin, type declarations can be placed anywhere in the file, before or after the function [main]; we place them after it;
  • Line 64: A functional interface can have methods with a body (the equivalent of Java’s [default] methods). The method [then] constructs a new [Operation]: this object performs the current operation, then applies the function [next] to its result. It returns a lambda converted to [Operation], which we will examine in the next section. Note that the [next] parameter does not require an interface like the [IntUnaryOperator] in the Java version: the functional type [(Int) -> Int] is sufficient, and it is executed via [next(…)];
  • lines 76–89: the class [Calculations] has two instance methods, [add] and [subtract], and a method of its companion object, [increase] (the equivalent of a static method, see Chapter 4), all three of which have the same form as the method [calculate];
  • line 9: the variable [op] is of type [Operation]. It is assigned the object obtained by converting the reference [Calculations::increase] to SAM, which refers to the method [increase] of the companion object of the class [Calculations]. The conversion must be written as follows: [Operation(Calculations::increase)] resembles a constructor call, but an interface does not have a constructor; the compiler creates the object that implements [Operation];
  • line 11: the operation is performed by the [calculate] method. The [increase] method is the one that executes, with the parameters passed to [calculate];
  • Line 15: An instance of class [Calculations] with the identifier [c1] is created;
  • Line 17: The reference [c1::add] refers to the method [add] of the object [c1]: this is called a “bound reference.” [increase] was a method of the companion object, while [add] is an instance method: this makes no difference. In the second case, the object created by the compiler also stores the object [c1] on which the method will be called;
  • line 21: the same is done with the instance method [subtract];
  • lines 26–32: To execute multiple operations, they are placed in a list of type [List<Operation>] (inferred type), which is then traversed. References and lambdas can be mixed (line 29). Each result is retrieved (lines 7–11 of the results);
  • line 36: the method [then] is applied to the operation [Operation(c1::add)]. The lambda [{ x -> x * x }] is passed outside the parentheses because it is the last (and in this case, the only) parameter of [then]: we will explain this syntax in more detail in the next section. [squaredSum.calculate(1, 2)] calculates (1+2)², which is 9 (lines 12–13 of the results). In Java, the conversion [((Operation) c1::add).then(…)] had to be written explicitly; in Kotlin, the explicit conversion SAM serves this purpose;
  • Line 40: [Operation] and the Java interface [IntBinaryOperator] have exactly the same form, but they are two different types with no inheritance relationship. The assignment [val other: IntBinaryOperator = op] would be rejected ([initializer type mismatch: expected 'IntBinaryOperator', actual 'Operation']). In contrast, the reference [op::calculate] is a function of type [(Int, Int) -> Int]: the conversion SAM turns it into an [IntBinaryOperator], which calls the [calculate] method of the [op] object ([c1::subtract] at this stage of the program);
  • lines 68–69: A functional interface can be generic. [Transformation<T, R>] represents a function that takes a [T] and returns an [R]: this is equivalent to [(T) -> R], with names chosen by us;
  • Line 44: [String::length] refers to the property [length] of the class [String]: a reference can point to a property as well as a function. It is used as a function of type [(String) -> Int]: the property will be read from the string passed as a parameter. The types [T = String] and [R = Int] of [Transformation] are inferred from the reference. Line 45: ["*"::repeat] refers to the method [repeat(Int)] of the string object ["*"]: it takes an integer and returns a string. These are two different types of references (unbound, bound), which we will discuss in detail in the following section;
  • line 73: a type alias. The parameters named [n1] and [n2] are for documentation purposes only;
  • lines 50–54: the lambda [f] of type [BinaryOperation] is assigned without conversion to the variable [g] of type [(Int, Int) -> Int]: they are the same type. To convert it to an [Operation], however, a conversion to SAM is required (line 54).

These rules can be verified by triggering errors. If you write ***[var op: Operation = Calculations::increase]*** without an explicit conversion, the compiler will reject it:

1
2
3
Lambdas02.kt:9:23: error: initializer type mismatch: expected 'Operation', actual 'KFunction2<Int, Int, Int>'.
    var op: Operation = Calculations::increase
                      ^

The message also reveals the type of a function reference: [KFunction2<Int, Int, Int>], a Kotlin reflection type ([kotlin.reflect]) that is also a [(Int, Int) -> Int]. The implicit SAM conversion occurs only for the arguments of a function call (for example, [values.removeIf(isEven)]), not for an assignment.

If a second abstract method, such as [fun invert(n: Int): Int], is added by mistake to the interface [Operation], the compiler flags the declaration itself and each of the SAM conversions that use the interface (abbreviated output):

1
2
3
4
5
6
7
8
Lambdas02.kt:9:14: error: interface 'fun interface Operation : Any' does not have constructors.
    var op = Operation(Calculations::increase)
             ^^^^^^^^^
...
Lambdas02.kt:60:1: error: functional interface must have exactly one abstract function.
fun interface Operation {
^^^
...

These messages are from the [kotlinc] compiler; Gradle displays them preceded by [e:] and the full file path. Like those from [javac], they are in English.

7.3. Anonymous objects, lambda expressions, and references

7.3.1. From anonymous objects to lambdas

In the previous examples, each function is a named function defined elsewhere in the program. For short functions used only once, this is cumbersome. Java originally offered the anonymous class: the class that implements the interface is defined right where the object is created. Kotlin has an equivalent—the object expression [object : Type { … }], introduced in Chapter 4—but it rarely uses it to represent a function: from the outset, Kotlin has supported lambda expressions, which offer a much more concise syntax. The program [Lambdas03.kt] demonstrates the different forms:

package lambdas03

// from anonymous objects to lambda expressions

fun main() {
    // 1 - an anonymous object: the class implementing the function type is written on the spot
    val add = object : (Int, Int) -> Int {
        override fun invoke(n1: Int, n2: Int): Int {
            return n1 + n2
        }
    }
    println("anonymous object: add(2, 3)=${add(2, 3)}")

    // 2 - the same thing with a lambda expression: { parameters -> body }
    val subtract = { n1: Int, n2: Int -> n1 - n2 }
    println("lambda with typed parameters: subtract(2, 3)=${subtract(2, 3)}")

    // 3 - the parameter types are inferred from the expected type
    val multiply: (Int, Int) -> Int = { n1, n2 -> n1 * n2 }
    println("lambda with untyped parameters: multiply(2, 3)=${multiply(2, 3)}")

    // 4 - a single parameter: it need not be named, it is then called it
    val square: (Int) -> Int = { it * it }
    val isEmpty: (String) -> Boolean = { s -> s.isEmpty() }
    println("square(7)=${square(7)}, isEmpty(\"\")=${isEmpty("")}")

    // 5 - no parameter: no arrow
    val greet = { "Hello" }
    val beep = { println("beep!") }
    println(greet())
    beep()

    // 6 - a body of several statements: the value of the last expression is the result
    val repeat: (String, Int) -> String = { text, times ->
        val buffer = StringBuilder()
        for (i in 1..times) {
            buffer.append(text)
        }
        buffer.toString()
    }
    println(repeat("ab", 3))

    // 7 - unused parameters: _
    val listener: (Any?, String) -> Unit = { _, _ -> println("event received") }
    listener(null, "click")

    // 8 - a lambda passed as a parameter: outside the parentheses if it is the last parameter
    val numbers = mutableListOf(3, 8, 1, 6, 5, 4)
    val evens = numbers.filter { it % 2 == 0 }
    println("evens: $evens")
    numbers.sortWith { n1, n2 -> n2.compareTo(n1) }
    println("descending order: $numbers")
    // the lambda outside the parentheses, after other parameters
    val total = numbers.fold(0) { acc, n -> acc + n }
    println("total: $total")

    // 9 - return in a lambda: return@label exits only the lambda
    numbers.forEach {
        if (it % 2 == 0) return@forEach
        print("$it ")
    }
    println("<- odds")
    // a label chosen by the developer
    numbers.forEach odd@{ n ->
        if (n % 2 != 0) return@odd
        print("$n ")
    }
    println("<- evens")
    // an anonymous function: return there exits the anonymous function
    val isLarge = fun(n: Int): Boolean {
        if (n > 5) return true
        return false
    }
    println("large: ${numbers.filter(isLarge)}")

    // 10 - destructuring the parameters of a lambda
    val ages = mapOf("Paul" to 40, "Mélanie" to 35)
    ages.forEach { (name, age) -> println("$name is $age years old") }
}

Results:

> gradlew -q run -Pmain=lambdas03.Lambdas03Kt
anonymous object: add(2, 3)=5
lambda with typed parameters: subtract(2, 3)=-1
lambda with untyped parameters: multiply(2, 3)=6
square(7)=49, isEmpty("")=true
Hello
beep!
ababab
event received
evens: [8, 6, 4]
descending order: [8, 6, 5, 4, 3, 1]
total: 27
5 3 1 <- odds
8 6 4 <- evens
large: [8, 6]
Paul is 40 years old
Mélanie is 35 years old

Comments:

  • lines 7–12: an anonymous object. The keyword [object] is followed by a colon and the type to be implemented—in this case, the functional type [(Int, Int) -> Int] (this is an interface, as we have seen), and the body of a class that implements this type by redefining its method [invoke]. The compiler generates an unnamed class and creates an instance of it. Six lines to express [n1 + n2]. Yet it is executed like a function: [add(2, 3)];
  • line 15: the same thing with a lambda expression. A lambda is written between curly braces: the list of parameters, the [->] operator (an arrow, which is read as “gives”), then the body. The type name and method name have disappeared. Since the parameters are typed, the compiler can determine the lambda’s type on its own: [(Int, Int) -> Int]. This differs from Java, where a lambda never has a type of its own;
  • line 19: the parameter types have been omitted. The compiler infers them from the expected type declared for the variable: [n1] and [n2] are of type [Int]. This is the most common form. In Kotlin, there is no distinction between an “expression body” and a “block body” as there is in Java: the body of a lambda is a sequence of statements, and the value of the last expression is the result of the lambda;
  • Line 23: With a single parameter, you don’t need to declare it at all, nor the arrow; it is then implicitly named [it] (“it,” “that”). [{ it * it }] reads as “its square.” Line 24: The single parameter is named [s]. This is done when [it] would make the code less readable, particularly when lambdas are nested (each would have its own [it]);
  • Lines 28–29: Without a parameter, there is no arrow. The type of *[greet]* is inferred as *[() -> String]*; the type of *[beep] is *[() -> Unit]*, since *[println] yields *[Unit]*;
  • Lines 34–39: A lambda can contain any instructions: declarations, loops, tests, etc. The value of the last expression, [buffer.toString()], is the result. You cannot write [return buffer.toString()]: in a lambda, an unlabeled [return] does not exit the lambda (see below);
  • line 44: the underscore character [_] denotes an unused parameter. It is used when the type signature requires parameters that the lambda does not need, which is common for event listeners. You can include multiple such parameters in the same lambda (Java adopted this syntax in version 22);
  • Lines 48–49: The most common use of lambdas is to pass them directly as parameters to a higher-order function. When the lambda is the last parameter, it is written outside the parentheses; if there are no other parameters, the parentheses are omitted: [numbers.filter { it % 2 == 0 }] rather than [numbers.filter({ it % 2 == 0 })]. It is this rule that gives Kotlin code its distinctive look and allows you to write functions that resemble language statements ([repeat], [use], [thread], [synchronized]…). Compare this with [Lambdas01.kt]: it is no longer necessary to write a function [isEven];
  • line 51: a lambda that reverses the usual comparison: the list is sorted in descending order. [sortWith] expects a [Comparator] (a Java interface): the conversion to SAM is done automatically;
  • line 54: the lambda remains outside the parentheses when the function has other parameters: [fold(0) { acc, n -> acc + n }]. The function [fold] (Chapter 5) starts with the initial value 0 and adds the elements one by one;
  • lines 58–62: within a lambda, the statement [return@forEach] exits the lambda (and thus moves on to the next element), just like a [continue] in a loop. The label [@forEach] is implicit: it is the name of the function to which the lambda is passed. An unlabeled [return] would exit the entire function [main]: this is called a non-local return, and it is only allowed in a lambda passed to a function like [inline] or [forEach] (we’ll see why);
  • lines 64–68: You can also choose the label by placing it before the opening curly brace: [odd@{ … }], then [return@odd];
  • lines 70–74: an anonymous function. It is written like a regular function, but without a name. Its type is inferred: [(Int) -> Boolean]. In an anonymous function, [return] exits the anonymous function itself. This is used when the body has multiple exit points, or when you want to explicitly specify the type of the result;
  • Line 78: The parameters of a lambda can be destructured (Chapter 3). The lambda passed to [forEach] receives an entry from the associative array ([Map.Entry<String, Int>]); The notation [(name, age)], enclosed in parentheses, decomposes it into its key and value. Note: [{ name, age -> … }] would declare two parameters, while [{ (name, age) -> … }] would declare a single decomposed parameter.

Let’s review the forms of a lambda expression:

Form
Example
Typed parameters
[{ a: Int, b: Int -> a + b }]
Untyped parameters (expected type known)
[{ a, b -> a + b }]
a single parameter, implicit
[{ it * it }]
no parameters
[{ "Hello" }]
unused parameters
[{ _, _ -> println("received") }]
destructured parameter
[{ (key, value) -> "$key=$value" }]
multiple statements
[{ x -> println(x) ; x * 2 }]
lambda as the last parameter
[list.filter { it > 0 }]
anonymous function
[fun(x: Int): Int { return x * 2 }]

A lambda is not quite a shorthand for an anonymous object. In an anonymous object, [this] refers to the anonymous object itself; in a lambda, [this] refers to the object of the method that encloses the lambda (or the receiver, for a lambda with a receiver). Furthermore, an anonymous object can implement an interface with multiple methods, multiple interfaces at once, or inherit from a class—which is impossible with a lambda. In this course, we will use lambdas whenever possible.

7.3.2. Additional Information: Type Inference and References

The program [Lambdas04.kt] explains how the type of a lambda is determined and describes the different types of references:

package lambdas04

import java.io.IOException
import java.util.concurrent.Callable
import java.util.function.Supplier

// more about lambdas and references

fun main() {
    // 1 - the type of a lambda is inferred from its parameters and its body...
    val double = { x: Int -> x * 2 }
    println("double(21)=${double(21)}")
    // ... or fixed by the expected type: the same lambda text can give three different types
    val function: () -> String = { "Hello" }
    val supplier = Supplier { "Hello" }
    val callable = Callable { "Hello" }
    println("${function()} / ${supplier.get()} / ${callable.call()}")

    // 2 - the kinds of references
    // a) top-level function: ::function
    val triple: (Int) -> Int = ::triple
    println("triple(14)=${triple(14)}")
    // b) method or property of a given object (bound reference): object::method
    val name = "Pierre"
    val inUpperCase: () -> String = name::uppercase
    val nameLength: () -> Int = name::length
    println("inUpperCase()=${inUpperCase()}, nameLength()=${nameLength()}")
    // c) method of any object of the class (unbound reference): Class::method
    //    the object on which the method is called becomes the first parameter
    val upperCase: (String) -> String = String::uppercase
    val ignoreCase: (String, String) -> Int = { s1, s2 -> s1.compareTo(s2, ignoreCase = true) }
    println("upperCase(\"paul\")=${upperCase("paul")}")
    println("ignoreCase(\"abc\", \"ABC\")=${ignoreCase("abc", "ABC")}")
    // d) extension function: Class::extension
    val convert: (String) -> Int = String::toInt
    println("convert(\"42\") + 1 = ${convert("42") + 1}")
    // e) constructor: ::Class
    val createList: () -> MutableList<String> = ::ArrayList
    val createBuffer: (String) -> StringBuilder = ::StringBuilder
    val createArray: (Int) -> IntArray = ::IntArray
    val createPerson: (String, Int) -> Person = ::Person
    val list = createList()
    list.add("element")
    println("$list ${createBuffer("abc").reverse()} ${createArray(3).size} ${createPerson("Paul", 40)}")

    // 3 - no checked exceptions: any lambda can throw any exception
    val read: () -> Int = { throw IOException("file missing") }
    try {
        read()
    } catch (ex: IOException) {
        println("exception received: ${ex.message}")
    }

    // 4 - equality of functions: references are comparable, lambdas are not
    val r1: () -> String = name::uppercase
    val r2: () -> String = name::uppercase
    val l1 = { name.uppercase() }
    val l2 = { name.uppercase() }
    println("r1 == r2 : ${r1 == r2}, r1 === r2 : ${r1 === r2}, l1 == l2 : ${l1 == l2}")
}

fun triple(n: Int) = n * 3

// a data class, for the reference to its constructor
data class Person(val name: String, val age: Int)

Results:

> gradlew -q run -Pmain=lambdas04.Lambdas04Kt
double(21)=42
Hello / Hello / Hello
triple(14)=42
inUpperCase()=PIERRE, nameLength()=6
upperCase("paul")=PAUL
ignoreCase("abc", "ABC")=0
convert("42") + 1 = 43
[element] cba 3 Person(name=Paul, age=40)
exception received: file missing
r1 == r2 : true, r1 === r2 : false, l1 == l2 : false

Comments:

  • line 11: the lambda [{ x: Int -> x * 2 }] has its own type, [(Int) -> Int], calculated from the type of its parameter and that of its body. The declaration [val double = { x -> x * 2 }], on the other hand, is rejected: the compiler cannot infer the type of [x] ([an explicit type is required on a value parameter]). In Java, [var twice = x -> x * 2;] is rejected for a more fundamental reason: a Java lambda cannot exist without a target type, which specifies the interface to be implemented;
  • lines 14–16: the same lambda expression, [{ "Hello" }], yields [() -> String] here, [Supplier<String>] there, and [Callable<String>] elsewhere. In the last two cases, it is the name of the Java interface—placed before the lambda—that triggers the conversion to SAM. These are three objects of different classes, which share only the form of their method;
  • lines 19–41: the types of references. Kotlin calls them “callable references”: they refer to functions, but also to properties and constructors:
    • line 21: the reference to a top-level function, [::function]. [::triple] is equivalent to the lambda [{ n -> triple(n) }]. It replaces the reference to a static method in Java ([Class::staticMethod]). For a function of a companion object, we write [Class::function], as in [Lambdas02.kt];
    • lines 25–26: the reference to a method or property of a given object, [object::method], called a bound reference. [name::uppercase] is equivalent to [{ name.uppercase() }]: the object [name] is fixed once and for all. [name::length] references the property [length] of the string [name];
    • Lines 30–31: a reference to a method of any object of a class, [Class::method], known as an unbound reference. The object on which the method will be called is not yet known: it becomes the first parameter of the function. [String::uppercase] is equivalent to [{ s: String -> s.uppercase() }]. For [ignoreCase], a lambda was preferred, which allows the argument named [ignoreCase = true] to be passed to [compareTo];
    • line 35: an extension function is referenced like a method: [String::toInt] is equivalent to [{ s: String -> s.toInt() }];
    • Lines 38–41: The reference to a constructor, [::Class] (in Java: [Class::new]). [::ArrayList] is equivalent to [{ ArrayList() }]; [::StringBuilder], with the expected type [(String) -> StringBuilder], selects the constructor that takes a string; [::IntArray] creates an array of integers of the size passed as a parameter; [::Person] refers to the primary constructor of the [Person] data class (line 65);
  • line 44: the creation functions are used; [reverse] reverses the contents of [StringBuilder] (line 8 of the results);
  • Lines 47–51: Kotlin does not support checked exceptions (Chapter 3). A lambda of type [() -> Int] can therefore throw an [IOException] without declaring anything. In Java, this was a real challenge: no [java.util.function] interface allows checked exceptions, and a lambda of type [Supplier<Integer>] that throws an [IOException] is rejected; so we had to use [Callable], whose method is declared as [throws Exception]. Note also that the lambda [{ throw IOException("file missing") }] is accepted as a [() -> Int]: the expression [throw] is of type [Nothing], which is compatible with all types;
  • lines 55–59: two evaluations of the same reference ***[name::uppercase] produce two different objects ([r1 === r2] is equal to *[false]*), but, unlike Java, Kotlin considers them *equal in the sense of [equals] ([r1 == r2] is equal to [true]): two references are equal if they refer to the same function, associated with the same object. Two lambdas, even with identical text, are never equal (line 10 of the results). Keep this in mind: it will be important when unregistering an event listener. Finally, note that the type of *[r1] must have been written: the function [uppercase] is overloaded ([uppercase()] and [uppercase(locale: Locale)]), and without an expected type, the compiler refuses to choose ([overload resolution ambiguity between candidates]*).

A reference is simply a shorthand notation for a lambda that merely calls a function. It is used when it improves readability: [String::length] rather than [{ it.length }]. In practice, with [it], Kotlin lambdas are so short that references are used slightly less often than in Java.

7.3.3. Composing Functions

Java’s functional interfaces have methods [default] and [static] that create new functions from existing ones: [andThen], [compose], [and], [or], [negate]… Kotlin’s functional types do not have these: the standard library does not provide a function [andThen] for a [(A) -> B]. But it is very easy to write them yourself, as extension functions on functional types (Chapter 4), since a function can return a function. The program [Lambdas05.kt] demonstrates this, then introduces the functions from the Kotlin library that build comparators:

package lambdas05

import java.util.function.Function

// composing functions: functions that return functions

fun main() {
    // 1 - composition: our extensions then (first this, then the other) and after (first the other)
    val plusOne: (Int) -> Int = { it + 1 }
    val timesTwo: (Int) -> Int = { it * 2 }
    println("(plusOne then timesTwo)(5)=${(plusOne then timesTwo)(5)}")
    println("(plusOne after timesTwo)(5)=${(plusOne after timesTwo)(5)}")
    // the methods of the Java Function interface remain available
    val f = Function<Int, Int> { it + 1 }.andThen { it * 2 }
    println("Function.andThen: f.apply(5)=${f.apply(5)}")
    // a function that makes functions
    val plusTen = adder(10)
    println("adder(10)(5)=${plusTen(5)}, adder(3)(5)=${adder(3)(5)}")
    // a processing chain built from a list of functions
    val steps: List<(String) -> String> = listOf(String::trim, String::lowercase, { it.replace(' ', '-') })
    val normalize = steps.fold({ s: String -> s }) { function, step -> function then step }
    println("normalize(\"  My Nice Title \")=${normalize("  My Nice Title ")}")

    // 2 - predicates: our extensions and, or, not
    val short: (String) -> Boolean = { it.length <= 4 }
    val startsWithP: (String) -> Boolean = { it.startsWith("P") }
    val firstNames = listOf("Paul", "Mélanie", "Jo", "Sébastien", "Pierre")
    println("short and P: ${filterElements(firstNames, short and startsWithP)}")
    println("short or P: ${filterElements(firstNames, short or startsWithP)}")
    println("not short: ${filterElements(firstNames, !short)}")
    // a predicate on CharSequence is suitable for filtering Strings
    val notEmpty: (CharSequence) -> Boolean = { it.isNotEmpty() }
    println("not empty: ${filterElements(listOf("a", "", "b"), notEmpty)}")

    // 3 - several processings of the same value
    val log = mutableListOf<String>()
    val display: (String) -> Unit = { println("display: $it") }
    val remember: (String) -> Unit = { log.add(it) }
    val both = display andAlso remember
    both("first")
    both("second")
    println("log: $log")

    // 4 - comparators: compareBy, thenBy, thenByDescending, reversed
    val persons = mutableListOf(Person("Paul", 32), Person("Mélanie", 25),
        Person("Jo", 32), Person("Sébastien", 25))
    persons.sortWith(compareBy(Person::age).thenBy(Person::name))
    println("by age then name: $persons")
    persons.sortWith(compareBy(Person::age).reversed().thenByDescending(Person::name))
    println("by descending age then descending name: $persons")
    // several criteria at once
    persons.sortWith(compareBy({ it.name.length }, { it.name }))
    println("by name length then name: $persons")
    // a comparator is also used to choose the smaller of two elements
    val byAge = compareBy(Person::age)
    println("younger of Paul and Mélanie: ${minOf(persons[1], persons[2], byAge)}")
}

// composition: first this, then next
infix fun <A, B, C> ((A) -> B).then(next: (B) -> C): (A) -> C = { x -> next(this(x)) }

// reverse composition: first before, then this
infix fun <A, B, C> ((B) -> C).after(before: (A) -> B): (A) -> C = { x -> this(before(x)) }

// the logical operations on predicates
infix fun <T> ((T) -> Boolean).and(other: (T) -> Boolean): (T) -> Boolean = { this(it) && other(it) }

infix fun <T> ((T) -> Boolean).or(other: (T) -> Boolean): (T) -> Boolean = { this(it) || other(it) }

operator fun <T> ((T) -> Boolean).not(): (T) -> Boolean = { !this(it) }

// two successive processings of the same value
infix fun <T> ((T) -> Unit).andAlso(other: (T) -> Unit): (T) -> Unit = { this(it); other(it) }

// a function that returns a function
fun adder(n: Int): (Int) -> Int = { it + n }

// keeps the elements that satisfy the predicate
fun <T> filterElements(elements: List<T>, predicate: (T) -> Boolean): List<T> {
    val result = mutableListOf<T>()
    for (element in elements) {
        if (predicate(element)) {
            result.add(element)
        }
    }
    return result
}

data class Person(val name: String, val age: Int) {
    override fun toString() = "$name($age)"
}

Results:

> gradlew -q run -Pmain=lambdas05.Lambdas05Kt
(plusOne then timesTwo)(5)=12
(plusOne after timesTwo)(5)=11
Function.andThen: f.apply(5)=12
adder(10)(5)=15, adder(3)(5)=8
normalize("  My Nice Title ")=my-nice-title
short and P: [Paul]
short or P: [Paul, Jo, Pierre]
not short: [Mélanie, Sébastien, Pierre]
not empty: [a, b]
display: first
display: second
log: [first, second]
by age then name: [Mélanie(25), Sébastien(25), Jo(32), Paul(32)]
by descending age then descending name: [Paul(32), Jo(32), Sébastien(25), Mélanie(25)]
by name length then name: [Jo(32), Paul(32), Mélanie(25), Sébastien(25)]
younger of Paul and Mélanie: Mélanie(25)

Comments:

  • Line 60: The extension function [then] is applied to a function of type [(A) -> B] (the parentheses around the function type are required) and takes a function [next] of type [(B) -> C]. It returns a new function of type [(A) -> C]—the lambda [{ x -> next(this(x)) }]—which first applies the function [this] (the extension’s receiver), then [next] to its result. This is the [andThen] method of the Java interface [Function]. The three parameters of type [A], [B], and [C] are generic: [then] composes any single-parameter functions, provided that the result of the first function is suitable for the second. The modifier [infix] allows you to write [f then g] instead of [f.then(g)];
  • line 63: the inverse composition, equivalent to the Java method [compose]: [f after g] first applies [g], then [f];
  • line 11: [plusOne then timesTwo] returns the function that first applies [plusOne], then [timesTwo] to the result: (5 + 1) × 2 = 12. The parentheses around the composition are necessary to be able to call the resulting function;
  • Line 12: [plusOne after timesTwo] first applies [timesTwo], then [plusOne]: 5 × 2 + 1 = 11;
  • line 14: if you prefer the Java interface methods, simply create a [Function] by converting SAM: [andThen] is then available, and you can pass it a Kotlin lambda, which is in turn converted. The result is a *[Function], which is executed by *[apply];
  • lines 17–18 and 76: [adder] is a function that returns a function: [adder(10)] returns the function that adds 10 to its parameter. The returned function can be stored in a variable ([plusTen]) or called immediately: [adder(3)(5)]. This process, which involves fixing one parameter to obtain a function of the remaining parameters, is called partial application;
  • lines 20–21: a dynamically constructed processing chain. We start with the identity function ([{ s: String -> s }], which returns its parameter unchanged) and, using [then], add each step in the list to it, thanks to the function [fold] (Chapter 5): The accumulated value, [function], is a function here. The resulting function [normalize] removes leading and trailing spaces ([trim]), converts the text to lowercase, and replaces spaces with dashes. Each call to [then] creates a new function: existing functions are never modified. Similarly, the list of steps could be read from a configuration file;
  • Lines 66–70: The predicates combine with our extensions [and], [or], and [not]. The latter is declared as [operator]: this is the function called by the operator [!] (see operator overloading in Chapter 4). Thus, [!short] is written for the negation of the predicate [short] (line 30). In Java, [isShort.negate()] is written, and [Predicate.not(String::isEmpty)] for a method reference;
  • lines 32–33: a predicate of type [(CharSequence) -> Boolean] is accepted by [filterElements] to filter a list of [String], whereas the parameter is declared as [(T) -> Boolean] with [T = String]. This is because Kotlin’s functional types are contravariant in their parameters and covariant in their results (Chapter 4): [(T) -> R] is actually [Function1<in T, out R>]. A function that can process any [CharSequence] can also process a [String]. In Java, you must declare the parameter [Predicate<? super T>] to achieve this result; in Kotlin, it happens automatically;
  • lines 36–42 and 73: the [andAlso] extension returns a function that performs two successive operations on the same value, like the [andThen] method of the Java interface [Consumer]. This is the simplest way to execute multiple operations with a single call. Note that the lambda [{ this(it); other(it) }] has two statements on the same line, separated by a semicolon;
  • line 47: the [compareBy(f)] function from the Kotlin library constructs a comparator that compares the values of [f] (here, age); [thenBy] specifies the tiebreaker to use in case of a tie (here, name). These are the equivalents of [Comparator.comparing] and [thenComparing] in Java. The references [Person::age] and [Person::name] refer to the properties of the data class [Person] (lines 89–90), for which we have redefined the method [toString] to shorten the output;
  • line 49: [reversed] reverses the order obtained so far, and [thenByDescending] adds a criterion in descending order;
  • line 52: [compareBy] also accepts multiple criteria, in the form of lambda expressions, applied in order: here, the length of the name, followed by the name. These are two lambdas passed as arguments, since [compareBy] receives them in a parameter named [vararg];
  • lines 55–56: The function [minOf] returns the smaller of its first two parameters based on the comparator passed as the third parameter (in Java: [BinaryOperator.minBy]);
  • lines 79–86: the function [filterElements] used for these tests. This is a higher-order function that does what the library function [filter] does: it applies the predicate [predicate] to each element—calling it as an ordinary function—and retains the elements for which it returns [true].

These functions allow you to write complex processing by combining simple functions, without writing a single class. We’ll revisit them when discussing collections and sequences.

7.3.4. Variable capture and closures

A lambda can use the local variables of the function in which it is written. It is said to capture these variables. In this regard, Kotlin differs significantly from Java. Java requires that a captured local variable be “effectively final”: it is never modified after its initialization, neither in the method nor in the lambda. Kotlin, like C#, JavaScript, or Python, imposes no such restriction: a lambda can capture a variable [var], read it even after it has changed, and even modify it. The program [Lambdas06.kt] demonstrates what this freedom allows, how it is implemented, and the pitfall it entails:

package lambdas06

// capturing variables and closures

// a top-level variable: lambdas can modify it
var nbCalls = 0

fun main() {
    // 1 - a lambda can use the local variables that surround it
    val threshold = 5
    val exceeds: (Int) -> Boolean = { n -> n > threshold }
    println("threshold=$threshold : exceeds(7)=${exceeds(7)}")

    // 2 - in Kotlin, the captured variable can be a var...
    var variableThreshold = 5
    val exceedsVariable: (Int) -> Boolean = { n -> n > variableThreshold }
    println("variableThreshold=$variableThreshold : exceedsVariable(7)=${exceedsVariable(7)}")
    // ... the lambda then sees the value of the variable at the time it runs
    variableThreshold = 10
    println("variableThreshold=$variableThreshold : exceedsVariable(7)=${exceedsVariable(7)}")

    // 3 - a lambda can modify a captured local variable
    var total = 0
    val accumulate: (Int) -> Unit = { n -> total += n }
    accumulate(3)
    accumulate(4)
    println("total=$total")
    // the captured variable is stored in a Ref object, which the lambda keeps
    println("fields of the lambda: ${accumulate::class.java.declaredFields.map { it.type.name }}")
    // a top-level variable (or a property) can also be modified
    val count = { nbCalls++ }
    count()
    count()
    println("nbCalls=$nbCalls")

    // 4 - a closure: the captured variable outlives the function that created it
    val counterA = createCounter()
    val counterB = createCounter()
    println("A: ${counterA()} ${counterA()} ${counterA()}")
    println("B: ${counterB()} ${counterB()}")

    // 5 - the for loop declares a new variable i on each iteration
    val actions = mutableListOf<() -> Unit>()
    for (i in 0..2) {
        actions.add { print("$i ") }
    }
    print("for: ")
    actions.forEach { it() }
    println()

    // the trap: all the lambdas share the same var
    actions.clear()
    var k = 0
    while (k < 3) {
        actions.add { print("$k ") }
        k++
    }
    print("while, capture of the var k: ")
    actions.forEach { action -> action() }
    println()
}

// a function that returns a function
fun createCounter(): () -> Int {
    // local variable captured by the returned lambda
    var value = 0
    return { ++value }
}

Results:

> gradlew -q run -Pmain=lambdas06.Lambdas06Kt
threshold=5 : exceeds(7)=true
variableThreshold=5 : exceedsVariable(7)=true
variableThreshold=10 : exceedsVariable(7)=false
total=7
fields of the lambda: [kotlin.jvm.internal.Ref$IntRef]
nbCalls=2
A: 1 2 3
B: 1 2
for: 0 1 2
while, capture of the var k: 3 3 3

Comments:

  • Line 11: The lambda uses the local variable [threshold] declared on line 10. [threshold] is not a parameter of the lambda; it is a captured variable. Since it is a [val], the lambda simply receives a copy of its value when it is created, exactly as in Java;
  • lines 15–19: this time, the lambda captures a *[var]*, which Java would reject (*[local variables referenced from a lambda expression must be final or effectively final]). It does not receive a copy of the value, but shares the variable with the function [main]: when [variableThreshold] changes to 10 (line 19), the same lambda returns a different result (lines 2–3 of the results). The lambda function therefore sees the value of the variable at the moment it executes*, not at the moment it was created. This is the issue that Java avoids by prohibiting these captures;
  • Lines 23–27: The lambda [accumulate] modifies the local variable [total]. In Java, you have to use a mutable object ([AtomicInteger], or the workaround of a single-element array); in Kotlin, you write [total += n] directly;
  • line 29: how is this possible, since the virtual machine itself only allows values to be captured? The Kotlin compiler does the work that a Java developer must do manually: a captured and modified local variable [var] is no longer stored on the stack, but in a small object of class [kotlin.jvm.internal.Ref.IntRef] (there is one for each primitive type, and *[Ref.ObjectRef] for references), which has a single mutable field, *[element]. The function [main] and the lambda share this object: every read or write to [total], in either one, becomes a read or write to [total.element]. We can verify this through reflection: the lambda object’s single field is of type [Ref$IntRef] (line 5 of the results). This is exactly what the C# compiler does with its captured variables;
  • lines 31–34: a lambda can also read and modify a top-level variable (line 6) or a property of the current object. There is nothing to capture here: the top-level variable is a static field of the [Lambdas06Kt] class; for a property, the lambda captures the reference [this];
  • lines 64–67: The function [createCounter] returns a lambda that captures and modifies its local variable [value]. Normally, a local variable disappears when the function ends. Here, the variable is stored in an object [Ref.IntRef] referenced by the lambda: it survives [createCounter] as long as the lambda exists. The combination of the lambda and the variables it captures is called a “closure”;
  • lines 37–40: Each call to *[createCounter] creates a new variable [value]*, and thus a new counter, independent of the others. The results (lines 7–8) confirm this;
  • lines 44–48: In a Kotlin loop [for], the variable [i] is a [val], which is new on each iteration: each lambda captures its own, and the three lambdas print 0, 1, and 2. Note that a list of functions is executed via [actions.forEach { it() }]: [it] is a function that is called;
  • Lines 53–59: This is the downside of Kotlin’s flexibility. The loop [while] uses a single variable, [k], which is modified on each iteration. The three lambdas capture the same variable and print its value when they execute—that is, after the loop, when it is equal to 3: they all print 3 (line 10 of the results). Java eliminates this pitfall at compile time; in Kotlin, it’s up to the developer to handle it. The solution is the same as in Java: declare a new [val] in the loop body for each iteration ([val copy = k]) and capture it, or, better yet, use a loop [for].

The Java rule has a solid justification: a lambda may be executed later, or in another thread (Chapter 8). If it can modify a function’s local variables, or read them while another thread is modifying them, this can lead to unpredictable behavior. Kotlin takes a different, more practical approach: capturing [var] is allowed, but [Ref] objects are not protected against concurrent access. So let’s remember that capturing a [var] is safe in a single-threaded program and in lambdas that are executed immediately ([forEach], [filter]…), but that as soon as a lambda can execute in another thread, the shared mutable state must be made explicit using an object whose threading behavior is explicitly chosen ([AtomicInteger], concurrent collection…). We will return to this topic in Chapters 8 and 9. The functions [inline], presented later, also avoid the creation of [Ref] objects when the lambda is copied into the calling code.

7.4. Inline Functions and Scope Functions

7.4.1. Inline functions

A lambda is an object. Passing a lambda to a higher-order function therefore incurs a cost: the creation of the object (except for a lambda that captures nothing, which the virtual machine can reuse), the call to its [invoke] method, the [Int] ↔ [Integer] conversions imposed by the generic interfaces [Function1], [Function2]…, and the [Ref] objects of the captured variables. For a function like [filter], which is called on every element of collections that can sometimes be very large, this cost matters. Kotlin eliminates this with the [inline] modifier: when a function is declared [inline fun], the compiler does not generate a call to that function; instead, it copies its code to every location where it is called, and it also copies the code of the lambdas passed to it. As a result, there are no longer any lambda objects, no [invoke] calls, and no conversions. The standard library functions that accept lambdas ([forEach], [filter], [map], [fold], [let], [apply], [repeat], [use], [synchronized]…) are almost all [inline]. Java has no equivalent: it relies on the virtual machine’s on-the-fly compiler to produce a result comparable to runtime execution.

The insertion of the code has a second, more noticeable consequence: since the lambda’s code is copied into the calling function, an unlabeled [return] instruction can exit the calling function from there. This is the non-local return encountered with *[forEach] in *[Lambdas03.kt]. Two modifiers complement *[inline]*:

  • [noinline], applied to a function parameter of a [inline] function, specifies that the corresponding lambda should not be copied, but passed as a regular object. This is necessary when the function wants to retain the lambda (store it in a list, return it as a result, etc.): copied code cannot be stored in a list;
  • [crossinline] requires that the lambda be copied, but prohibits non-local returns. This is necessary when the function [inline] does not execute the lambda itself, but executes it in another context—for example, within a [Runnable] object that it creates: When [Runnable] executes, the calling function may have long since finished, and a [return] would no longer be able to exit it.

The program [Lambdas07.kt] illustrates these concepts (this program has no equivalent in the Java course):

package lambdas07

// inline, noinline, crossinline functions

fun main() {
    // 1 - an ordinary function receives a function object; an inline function receives the code of the lambda
    execute { println("lambda passed to execute, executed by: ${currentMethod()}") }
    executeInline { println("lambda passed to executeInline, executed by: ${currentMethod()}") }

    // 2 - the non-local return is only allowed in a lambda passed to an inline function
    val numbers = listOf(3, 8, 1, 6)
    println("first even of $numbers: ${firstEven(numbers)}")

    // 3 - noinline: a lambda that must be kept as an object
    val callbacks = mutableListOf<() -> Unit>()
    executeThenStore(callbacks, { println("executed right away") }, { println("stored callback") })
    println("${callbacks.size} stored callback, which we execute:")
    callbacks.forEach { it() }

    // 4 - crossinline: a lambda executed in another context
    val task = asTask { println("executed by ${Thread.currentThread().name}") }
    val thread = Thread(task, "worker")
    thread.start()
    thread.join()
}

// the name of the method that calls currentMethod (read from the call stack)
fun currentMethod(): String = Throwable().stackTrace[1].methodName

// an ordinary function: block is an object of type () -> Unit
fun execute(block: () -> Unit) {
    block()
}

// an inline function: its code, and that of the lambda, are copied where it is called
inline fun executeInline(block: () -> Unit) {
    block()
}

// our own forEach, inline like the library's one
inline fun <T> forEachElement(elements: List<T>, action: (T) -> Unit) {
    for (element in elements) action(element)
}

fun firstEven(numbers: List<Int>): Int? {
    forEachElement(numbers) {
        // non-local return: we exit firstEven, and not only the lambda
        if (it % 2 == 0) return it
    }
    return null
}

// immediate is copied; stored remains a function object, which can be put in the list
inline fun executeThenStore(
    callbacks: MutableList<() -> Unit>,
    immediate: () -> Unit,
    noinline stored: () -> Unit,
) {
    immediate()
    callbacks.add(stored)
}

// action is executed in a Runnable object, not in asTask: a non-local return is impossible there
inline fun asTask(crossinline action: () -> Unit): Runnable = Runnable { action() }

Results:

1
2
3
4
5
6
7
8
> gradlew -q run -Pmain=lambdas07.Lambdas07Kt
lambda passed to execute, executed by: main$lambda$0
lambda passed to executeInline, executed by: main
first even of [3, 8, 1, 6]: 8
executed right away
1 stored callback, which we execute:
stored callback
executed by worker

Comments:

  • Line 28: The function [currentMethod] returns the name of the method that called it. To do this, it creates a [Throwable] object, which saves the execution stack at the time of its creation, and reads the second element of that stack (the first being [currentMethod] itself). We use this to determine where a lambda’s code is executed;
  • lines 31–36: two identical functions, one ordinary and the other declared as [inline];
  • line 7: the lambda is passed to the regular function [execute]. The compiler turns this into the body of a private method of the [Lambdas07Kt] class—named [main$lambda$0] here—and creates an object of type [() -> Unit] that calls it. It is indeed [main$lambda$0] that executes (line 1 of the results);
  • line 8: the lambda is passed to the function [inline] [executeInline]. The compiler copied the body of [executeInline] and that of the lambda into [main]: the lambda’s [println] executes directly within the [main] method (line 2 of the results). No function object was created;
  • lines 41–50: [forEachElement] is our own version of [forEach], [inline], just like the original. In [firstEven], the [return it] statement in the lambda is a non-local return: it exits the [firstEven] function by returning the first even number found (line 3 of the results), and the loop stops there. With an ordinary function like [execute], the compiler would reject such a [return] (['return' is prohibited here]): the lambda would be a separate object, which could be executed after [firstEven] has finished;
  • lines 54–60: a function [inline] that takes two lambdas: it executes the first one and stores the second in a list for later execution. The second parameter must be declared as [noinline]: without this modifier, the compiler refuses to store the lambda in the list ([illegal usage of inline parameter 'stored: () -> Unit'. Add 'noinline' modifier to the parameter declaration.]). Lines 16–18 use it (lines 4–6 of the output);
  • line 64: the function [asTask] creates a [Runnable] object (conversion SAM) that will execute the lambda [action]. The parameter must be declared as [crossinline]: without this modifier, the compiler rejects it ([cannot inline 'action: () -> Unit' here: it might contain non-local returns. Add 'crossinline' modifier to parameter declaration 'action: () -> Unit'.]). Lines 21–24 execute the [Runnable] obtained in a thread named [worker] (threads are covered in Chapter 8), as confirmed by line 7 of the results.

You shouldn’t use [inline] on just any function: the modifier is only useful for functions that accept functional parameters, and it increases the size of the generated code (the compiler, in fact, issues a warning for a [inline] function without functional parameters). Finally, it has a third use, discussed in Chapter 4: only [inline] functions can have parameters of type [reified] ([inline fun <reified T> …]), whose type remains known at runtime, since the code is copied to the location where that type is known.

7.4.2. Scope functions

The Kotlin standard library provides five small, widely used functions—[inline]—known as "scope functions": [let], [run], [with], [apply], and [also]. They do almost nothing: each one executes a lambda on an object. They differ in two ways:

  • the way the lambda refers to the object: either as its parameter [it] (the lambda is of type [(T) -> R]), or as its receiver [this], which can then be omitted (the lambda is a receiver lambda, of type [T.() -> R]; see [Lambdas01.kt]);
  • which is what the function returns: either the result of the lambda or the object itself.
Function
Object designated by
Returns
Typical usage
[let]
[it]
the result of the lambda
transform a value; with [?.let], process a non-null value
[run]
[this]
the result of the lambda
Calculate a result based on the object's properties
[with(object)]
[this]
the result of the lambda
such as [run], where the object is passed as a parameter
[apply]
[this]
the object
Configure a newly created object
[also]
[it]
the object
Additional processing (logging, caching, etc.) without modifying the call chain

The program [Lambdas08.kt] uses them (it has no equivalent in the Java course):

package lambdas08

// the scope functions: let, run, with, apply, also (and takeIf)

fun main() {
    // 1 - apply: configures the object (this) and returns the object
    val paul = Person("Paul").apply {
        age = 32
        city = "Angers"
    }
    println("apply: $paul")

    // 2 - also: an additional processing of the object (it), which is returned
    val persons = mutableListOf<Person>()
    val melanie = Person("Mélanie", 25).also { persons.add(it) }
    println("also: $melanie, persons=$persons")

    // 3 - let: transforms the object (it) into a result; widely used with ?.
    val length = paul.name.let { it.length * 2 }
    println("let: length=$length")
    for (city in listOf("Tours", null)) {
        val message = city?.let { "lives in ${it.uppercase()}" } ?: "unknown city"
        println("let and ?.: $message")
    }

    // 4 - run: a computation on the object (this), which returns a result
    val description = paul.run { "$name, $age years old, lives in $city" }
    println("run: $description")

    // 5 - with: like run, but the object is passed as a parameter
    val summary = with(StringBuilder()) {
        append("Persons: ")
        append(persons.joinToString { it.name })
        toString()
    }
    println("with: $summary")

    // 6 - takeIf: the object if it satisfies the predicate, null otherwise
    for (age in listOf(15, 40)) {
        val adult = Person("Jo", age).takeIf { it.age >= 18 }
        println("takeIf: ${adult ?: "minor"}")
    }
}

class Person(val name: String, var age: Int = 0) {
    var city: String? = null

    override fun toString() = "Person(name=$name, age=$age, city=$city)"
}

Results:

> gradlew -q run -Pmain=lambdas08.Lambdas08Kt
apply: Person(name=Paul, age=32, city=Angers)
also: Person(name=Mélanie, age=25, city=null), persons=[Person(name=Mélanie, age=25, city=null)]
let: length=8
let and ?.: lives in TOURS
let and ?.: unknown city
run: Paul, 32 years old, lives in Angers
with: Persons: Mélanie
takeIf: minor
takeIf: Person(name=Jo, age=40, city=null)

Comments:

  • lines 45–48: a regular [Person] class with mutable properties;
  • lines 7–9: [apply] executes the lambda with the object as the receiver: within the lambda, [age = 32] refers to [this.age = 32]. It then returns the object, which is assigned to [paul]. This creates and configures an object in a single expression. Its signature illustrates the mechanism: [inline fun <T> T.apply(block: T.() -> Unit): T];
  • line 15: [also] executes the lambda with the object as the parameter [it], then returns the object. This is used to achieve a side effect (in this case, adding to a list) without having to introduce an intermediate variable;
  • line 19: [let] executes the lambda with the object as the parameter [it] and returns the result of the lambda: this is a transformation;
  • Lines 21–23: The most common use of [let], as seen in Chapter 3: [city?.let { … }] executes the lambda only if [city] is not [null], and, within the lambda, [it] is of type [String] and not [String?]. If [city] equals [null], the expression equals [null], and the Elvis operator [?:] provides the default value;
  • Line 27: [run] executes the lambda with the object as the receiver and returns the result of the lambda: it directly uses the properties [name], [age], and [city] of [paul];
  • Lines 31–34: [with] does the same thing as [run], but the object is passed to it as the first parameter, and the lambda comes second, outside the parentheses. All the methods called within the lambda ([append], [toString]) are those of [StringBuilder]. The function [buildString] from Chapter 5 is built on this model;
  • lines 39–41: [takeIf] is not a scoped function, but it is used in the same way: it returns the object if it satisfies the predicate, and [null] otherwise. It combines naturally with [?:], [?.let]… ([takeUnless] does the opposite).

These functions make the code more compact, but they shouldn’t be overused: multiple nested scope functions, each with its own [it] or [this], quickly make the code unreadable. A well-named local variable is often preferable.

7.5. Functional types, functional interfaces, or interfaces?

In many languages, the concept of a “function passed as a parameter” (function pointer in C, delegate in C#) is distinct from the concept of an interface, and one might wonder which to choose. In Java, the question is framed differently, since the two concepts are one and the same: a lambda is an instance of an interface. Kotlin falls somewhere in between: it has functional types, which are distinct from interfaces in syntax but are interfaces to the virtual machine; and it has functional interfaces ([fun interface]) that can be implemented using lambdas. Let’s take the example [Lambdas09.kt], which is similar to [Lambdas02.kt]:

package lambdas09

// a parameter of function type

fun main() {
    // execution of the add function
    println(execute(2, 3, ::add))
    // execution of the subtract function
    println(execute(2, 3, ::subtract))
    // execution of a Java library method having the right shape
    println(execute(2, 3, Math::max))
    // execution of a lambda: no named function is needed
    println(execute(2, 3) { n1, n2 -> n1 * n2 })
}

// execution of an operation received as a parameter
// (last, so that a lambda can be passed outside the parentheses)
fun execute(n1: Int, n2: Int, op: (Int, Int) -> Int): Int = op(n1, n2)

// two functions having the shape (Int, Int) -> Int
fun add(n1: Int, n2: Int): Int {
    println("add($n1,$n2)")
    return n1 + n2
}

fun subtract(n1: Int, n2: Int): Int {
    println("subtract($n1,$n2)")
    return n1 - n2
}

Results:

1
2
3
4
5
6
7
> gradlew -q run -Pmain=lambdas09.Lambdas09Kt
add(2,3)
5
subtract(2,3)
-1
3
6

On line 18, the function [execute] expects a parameter of type [(Int, Int) -> Int]. This allows different functions to be passed to it: our high-level functions [add] and [subtract] (lines 7 and 9), and a method from the Java library with the correct form (line 11: among the overloads of [Math.max]*, the compiler chooses [int max(int a, int b)]***, which Kotlin views as ***[max(a: Int, b: Int): Int]***), and even a lambda (line 13): it wasn’t necessary to write a multiplication function. The function parameter was placed last** so that the lambda could be written outside the parentheses: this is the convention throughout the Kotlin library, and it’s a good idea to follow it in your own functions.

With [fun interface], we see the interface polymorphism covered in Chapter 4: the program [Lambdas10.kt] demonstrates this by implementing the same interface with classes:

package lambdas10

// classes that implement a functional interface or a function type

fun main() {
    // execution of the Add operation
    println(execute(Add(), 2, 3))
    // execution of the Subtract operation
    println(execute(Subtract(), 2, 3))
    // the same execute function still accepts lambdas
    println(execute({ n1, n2 -> n1 * n2 }, 2, 3))
    // an interface with two abstract methods no longer accepts a lambda: a class is needed
    val division = object : NamedOperation {
        override fun calculate(n1: Int, n2: Int) = n1 / n2

        override val name = "integer division"
    }
    println("${division.name}: ${division.calculate(7, 2)}")
    // a class can also implement a function type
    val modulo = Modulo()
    println("modulo(7, 2)=${modulo(7, 2)}, applyFunction(modulo)=${applyFunction(modulo, 7, 3)}")
}

// execution of the single method of the Operation interface
fun execute(op: Operation, n1: Int, n2: Int): Int = op.calculate(n1, n2)

// execution of a function of type (Int, Int) -> Int
fun applyFunction(f: (Int, Int) -> Int, n1: Int, n2: Int): Int = f(n1, n2)

// the functional interface Operation
fun interface Operation {
    fun calculate(n1: Int, n2: Int): Int
}

// an interface that is not functional: an abstract method and an abstract property
interface NamedOperation : Operation {
    val name: String
}

// class Add
class Add : Operation {
    override fun calculate(n1: Int, n2: Int): Int {
        println("Add($n1,$n2)")
        return n1 + n2
    }
}

// class Subtract
class Subtract : Operation {
    override fun calculate(n1: Int, n2: Int): Int {
        println("Subtract($n1,$n2)")
        return n1 - n2
    }
}

// a class that implements the function type (Int, Int) -> Int
class Modulo : (Int, Int) -> Int {
    override fun invoke(n1: Int, n2: Int): Int = n1 % n2
}

Results:

1
2
3
4
5
6
7
8
> gradlew -q run -Pmain=lambdas10.Lambdas10Kt
Add(2,3)
5
Subtract(2,3)
-1
6
integer division: 3
modulo(7, 2)=1, applyFunction(modulo)=1

Comments:

  • lines 31–32: The functional interface [Operation] defines a method [calculate];
  • lines 41–49: The classes [Add] and [Subtract] implement the interface [Operation];
  • line 25: the function [execute], whose first parameter is of the type defined by the interface [Operation]. It receives, in succession, an instance of the class [Add], an instance of the class [Subtract] (lines 7 and 9), and then a lambda (line 11). For the lambda, the conversion from SAM to *[Operation] is performed automatically, since it is a call argument. For [execute], these three objects are of the same type: objects that implement [Operation]*;
  • Lines 36–37: The [NamedOperation] interface inherits the [calculate] method and adds an abstract property [name]. It is not functional (in fact, it cannot be declared as [fun interface]): it can no longer be implemented using a lambda; a class is required—in this case, an anonymous object (lines 13–16);
  • lines 57–58: the converse is also true: a class can implement a functional type, since it is an interface. Its instances are then called like functions ([modulo(7, 2)]) and passed wherever a function is expected ([applyFunction(modulo, 7, 3)], line 21).

These two examples show that in Kotlin, just as in Java, a function that expects a behavior accepts objects from classes written for that purpose, references, and lambdas. Two questions remain. The first is specific to Kotlin: for a functional parameter, is a functional type required, or is a [fun interface] sufficient?

  • The functional type is the default choice. It requires no declaration, is called like a function, directly accepts lambdas and references, and benefits from [inline] functions. A [typealias] gives it a name when it is long;
  • the “fun” interface is justified when you want a truly distinct type (one that cannot be confused with another function of the same form), a method with a descriptive name, additional methods with a body, or a API also intended for Java callers: for Java, a [fun interface] is an ordinary functional interface, usable with its lambdas, whereas a Kotlin functional type appears to it as a [Function2<Integer, Integer, Integer>] whose [invoke] method must return an object ([Unit] for a “returnless” function).

The second question is the same as in Java: should the interface have a single method, or multiple ones?

  • A function, functional type, or single-method interface is more flexible to use: any existing function with the correct form will work, and a lambda even allows you to write the function right where it’s called. There is no class to write. This flexibility explains why modern libraries (collections, the coroutines in Chapter 9, Spring) make extensive use of functional parameters;
  • an interface with multiple members regains the advantage as soon as the behavior to be implemented involves several interrelated operations ([calculate] and [name] above), or when it requires its own state and identity.

This is the case for the [ITaxDao] interface in the [dao] layer from Chapter 6: although it has only one member—the [val taxBrackets: List<TaxBracket>] property—its implementations have state (the brackets read, the file name), are created by the dependency injection container, and represent a clearly named role within the application. A [() -> List<TaxBracket>] would technically have sufficed, but it would have been much less descriptive. We therefore reserve functional types for short, one-off behaviors (a criterion, a transformation, an action), and “business” interfaces for the application’s roles, regardless of the number of their members.

7.6. Event Handling

7.6.1. The Observer Pattern

Functions passed as parameters are used to set up events. Like Java, Kotlin has no dedicated keyword for events (C# has its own keyword, [event]): it uses the conventions established by Java as early as version 1.1 for graphical components and JavaBeans, known as the “listener model.” A [C1] class that wants to signal an [Xxx] event proceeds as follows:

  • A [XxxEvent] class encapsulates information about the event, including the object that triggered the event, known as the source. In Java, the convention is for it to derive from [java.util.EventObject], which stores the source; in Kotlin, a regular class with read-only properties is sufficient, and that is what we will do;
  • A listener type defines the form of the function that will be called when the event occurs. In Java, this is an interface named [XxxListener]; in Kotlin, it is most often a simple functional type, [(XxxEvent) -> Unit], named by a [typealias]: a listener will be a lambda or a function reference;
  • the [C1] class manages a list of functions and provides two public methods for registering and unregistering with it: [addXxxListener(listener)] and [removeXxxListener(listener)];
  • When a [c1] instance of [C1] wants to signal the event, it creates an [XxxEvent] object and calls each of the registered listeners, passing this object to them. By convention, this notification is handled by a protected method [fireXxx(e: XxxEvent)] (“fire”), which a derived class can override to modify how the event is published;
  • If an object [c2] wants to be notified of the event [Xxx] from the object [c1], it subscribes using [c1.addXxxListener(...)]. It can unsubscribe via [c1.removeXxxListener(...)] when it no longer wishes to be notified. Since [c1] manages a list, different objects can subscribe to be notified of the same event.

In this scenario, we have:

  • a class that signals (also called “publishes” or “emits”) an event;
  • objects that are notified of this event. They are said to subscribe to or register for the event: these are the listeners, or event handlers;
  • a type of listener that defines the form of the functions that will be notified of the event.

This is the Observer design pattern: the emitter does not know its subscribers; it simply notifies them. This model offers the same protection as a dedicated keyword: the list of listeners is a private property of the emitter. From the outside, one can only subscribe or unsubscribe; one cannot clear the list, replace other listeners, or trigger the event in place of the emitter.

7.6.2. An example: excessively high temperature

Consider the following example:

  • A class named [Emitter] encapsulates a temperature. When this temperature exceeds a certain threshold, an event must be triggered. We’ll call this event [TemperatureTooHigh]. Information about this event will be encapsulated in a [TemperatureTooHighEvent] object, and the listeners will be functions of type [TemperatureTooHighListener];
  • a class named [Subscriber] registers as a listener for the previous event. When it is notified of the event, it displays a message on the console. A listener unsubscribes automatically after receiving a specified number of alerts;
  • A console program creates an emitter and two subscribers. It enters temperatures via the keyboard and records them in the emitter. If any of them is too high, the emitter signals the [TemperatureTooHigh] event.

This program is divided into three files in the [lambdas11] package. First is the file [Emitter.kt], which contains the event class, the listener type, and the class [Emitter]:

package lambdas11

import java.time.LocalTime
import java.util.concurrent.CopyOnWriteArrayList

// the information carried by the event
class TemperatureTooHighEvent(
    // the emitter that originated the event
    val source: Emitter,
    // temperature at the time of the event
    val temperature: Double,
) {
    // time of the event
    val time: LocalTime = LocalTime.now()
}

// the shape of the event listeners: a simple function type
typealias TemperatureTooHighListener = (TemperatureTooHighEvent) -> Unit

// the class that signals the event
open class Emitter(val name: String) {
    companion object {
        // threshold that triggers the event
        const val THRESHOLD = 19.0
    }

    // the registered listeners: a list of functions
    private val listeners = CopyOnWriteArrayList<TemperatureTooHighListener>()

    // observed temperature: writing the property can trigger the event
    var temperature = 0.0
        set(value) {
            field = value
            if (value > THRESHOLD) {
                // the event is signaled to the listeners
                fireTemperatureTooHigh(TemperatureTooHighEvent(this, value))
            }
        }

    // registration of a listener
    fun addTemperatureTooHighListener(listener: TemperatureTooHighListener) {
        listeners.add(listener)
    }

    // unregistration of a listener
    fun removeTemperatureTooHighListener(listener: TemperatureTooHighListener) {
        listeners.remove(listener)
    }

    // notification of the event to all the registered listeners
    protected open fun fireTemperatureTooHigh(e: TemperatureTooHighEvent) {
        for (listener in listeners) {
            listener(e)
        }
    }
}

Comments:

  • Lines 7–14: The [TemperatureTooHighEvent] class stores the event source, of type [Emitter], and the temperature that triggered the event. We added the event time, [time], which is initialized when the object is created. All properties are of type [val]: a listener must not be able to modify the event information for subsequent listeners. We did not derive the class from [EventObject] as in Java: that would serve no purpose here, and would require us—in order for the source to be of type [Emitter] rather than [Any]—to redefine the [getSource] method;
  • Line 18: the type of the listener. A listener is a function that takes the event object as an argument and returns nothing. Where Java requires an interface *[TemperatureTooHighListener]* derived from *[EventListener]*, Kotlin simply uses a functional type alias. We could just as easily have declared a ***[fun interface TemperatureTooHighListener]***: that would be the right choice if the listeners were to be written in Java;
  • line 24: the temperature threshold above which the [TemperatureTooHigh] event is triggered, a constant of the companion object;
  • line 21: the emitter has a name for identification. The class is declared as [open], so that a derived class can redefine the [fireTemperatureTooHigh] method;
  • line 28: the list of registered listeners, a list of functions. It is a [CopyOnWriteArrayList] from the [java.util.concurrent] package, a list that, whenever it is modified, copies its contents into a new array. An ongoing iteration therefore continues on the old array, even if the list is modified in the meantime. We’ll see later why this is essential here. This list is the standard type for listener lists: the list is traversed for each event, but it is rarely modified;
  • Lines 31–36: The property [temperature] has a custom accessor [set] (Chapter 4): it stores the new value in the hidden field [field], then signals the event if it exceeds the threshold. Publishing is handled by the [fireTemperatureTooHigh] method, to which a [TemperatureTooHighEvent] object is passed; this object originates from the emitter itself ([this]) and contains the temperature that exceeded the threshold. Where Java requires a method named [setTemperature], the class user simply writes [e1.temperature = 21.0];
  • lines 41–42: Registering a listener adds it to the list. The parameter type is not nullable: the compiler rejects any listener of type [null], which Java must reject at runtime via [Objects.requireNonNull];
  • lines 46–47: Unregistering removes the listener from the list. The method [remove] searches for the object to be removed using [equals]: therefore, it must be passed an object identical to the one that was registered;
  • lines 51–53: The notification calls each registered listener, in the order they were registered, as a function: [listener(e)]. If there are no listeners, the loop does nothing: there are no special cases to handle.

Here is the file [Subscriber.kt]:

package lambdas11

// a class that registers as a listener of the event
class Subscriber(
    // name of the subscriber
    private val name: String,
    // maximum number of alerts accepted
    private val nbMaxAlerts: Int,
) {
    // number of alerts received
    private var nbAlerts = 0

    // registration with an emitter
    fun subscribe(emitter: Emitter) {
        emitter.addTemperatureTooHighListener(::onTemperatureTooHigh)
    }

    // unregistration: a new reference to the same method of the same object is equal to it
    fun unsubscribe(emitter: Emitter) {
        emitter.removeTemperatureTooHighListener(::onTemperatureTooHigh)
    }

    // handler of the TemperatureTooHigh event
    private fun onTemperatureTooHigh(e: TemperatureTooHighEvent) {
        val emitter = e.source
        val temperature = "%.1f".format(e.temperature)
        println("Subscriber [$name]: the source [${emitter.name}] " +
            "signaled a temperature that is too high: [$temperature]")
        // beyond nbMaxAlerts alerts, the subscriber unregisters
        nbAlerts++
        if (nbAlerts == nbMaxAlerts) {
            unsubscribe(emitter)
            println("Subscriber [$name]: unsubscribed after $nbAlerts alert(s)")
        }
    }
}

Comments:

  • lines 4–11: Each subscriber is identified by a name and is assigned the maximum number of alerts they accept ([nbMaxAlerts]). It counts the alerts received ([nbAlerts]);
  • lines 14–15: registration with an emitter. The listener is a bound reference, [::onTemperatureTooHigh], that is, [this::onTemperatureTooHigh]: this subscriber’s method, [onTemperatureTooHigh];
  • lines 19–20: Unsubscribing passes a new reference, [::onTemperatureTooHigh], to [removeTemperatureTooHighListener]. In Java, this would be a classic error: two evaluations of [this::onTemperatureTooHigh] produce two objects that [equals]* considers different, and the unsubscription would not remove anything, without any error message; the listener must be created only once and stored in a field. We saw with [Lambdas04.kt] that in Kotlin, two references to the same function of the same object are equal: [remove] therefore finds the registered listener—and only that listener—since the references of the other subscribers are bound to other objects. This would not be the case with a lambda: [{ e -> onTemperatureTooHigh(e) }]*, written twice, would result in two different objects;
  • Line 24: the event handler. It has the form dictated by the type [TemperatureTooHighListener]. It is private: only the subscriber can register it with an emitter. Another approach is to have the listener type implemented by the [Subscriber] class itself ([class Subscriber : TemperatureTooHighListener], with a method [invoke]) and register with [this]. This approach has the drawback of making the method [invoke] public, which anyone could then call;
  • line 25: the event source is directly of type [Emitter];
  • lines 26–28: the handler displays the subscriber’s name, the name of the emitter that reported the event, and the temperature that triggered it. The [%.1f] format displays the real number with one decimal place, according to the machine’s local conventions (the Kotlin function [format] uses them; here, a decimal point). The string is written in two parts concatenated by [+] so that the line fits on the page;
  • lines 30–33: After [nbMaxAlerts] alerts, the subscriber unsubscribes from the emitter. It does so during the event notification, from its own handler—that is, while the [fireTemperatureTooHigh] method iterates through the list of listeners. This is safe thanks to [CopyOnWriteArrayList]: [remove] creates a new array, but the current iteration continues on the old one.

Finally, here is the main program, [Lambdas11.kt]:

package lambdas11

// event handling: an emitter and listeners

fun main() {
    // creation of an event emitter
    val e1 = Emitter("e")
    // creation of two subscribers that register with e1
    val subscribers = listOf(Subscriber("s0", 3), Subscriber("s1", 1))
    for (subscriber in subscribers) {
        subscriber.subscribe(e1)
    }
    // a third listener: a lambda
    e1.addTemperatureTooHighListener { e ->
        println("Log: %.1f °C at %tT".format(e.temperature, e.time))
    }

    // the temperatures are read from the keyboard
    while (true) {
        print("Temperature (nothing to stop): ")
        // end of input? (readlnOrNull returns null if standard input is closed)
        val input = readlnOrNull()
        if (input.isNullOrBlank()) {
            break
        }
        // the decimal comma is accepted
        val temperature = input.trim().replace(',', '.').toDoubleOrNull()
        if (temperature == null) {
            // the error is reported
            println("Incorrect temperature")
        } else {
            // correct temperature - it is stored in the emitter
            e1.temperature = temperature
        }
    }
}

Results (the standard input was supplied via redirection; we have reinserted the inputs after the prompts, where the program reads them):

> gradlew -q run -Pmain=lambdas11.Lambdas11Kt
Temperature (nothing to stop): 17
Temperature (nothing to stop): 21
Subscriber [s0]: the source [e] signaled a temperature that is too high: [21.0]
Subscriber [s1]: the source [e] signaled a temperature that is too high: [21.0]
Subscriber [s1]: unsubscribed after 1 alert(s)
Log: 21.0 °C at 12:33:37
Temperature (nothing to stop): abc
Incorrect temperature
Temperature (nothing to stop): 22,5
Subscriber [s0]: the source [e] signaled a temperature that is too high: [22.5]
Log: 22.5 °C at 12:33:37
Temperature (nothing to stop): 25
Subscriber [s0]: the source [e] signaled a temperature that is too high: [25.0]
Subscriber [s0]: unsubscribed after 3 alert(s)
Log: 25.0 °C at 12:33:37
Temperature (nothing to stop): 30
Log: 30.0 °C at 12:33:37
Temperature (nothing to stop):

Comments:

  • line 7: creation of the emitter;
  • lines 9–11: creation of two subscribers, [s0] (up to 3 alerts) and [s1] (1 alert), which are registered with the emitter;
  • lines 14–15: a third listener, a lambda passed outside the parentheses, which acts as a log. The type of the parameter [e] is inferred from that of the parameter of [addTemperatureTooHighListener]. The format [%tT] displays a time in the form [HH:mm:ss]. This lambda cannot be unregistered: it would need to have been stored in a variable in order to pass the same object to [removeTemperatureTooHighListener]. Therefore, lambdas written directly in the registration call are reserved for listeners that remain registered as long as the emitter exists;
  • lines 19–33: loop for entering temperatures via the keyboard. [readlnOrNull()] reads a line; it returns [null] if standard input is closed, a case that is treated as an empty entry using [isNullOrBlank] (line 23). After this test, the compiler knows that [input] is not [null] (intelligent type conversion);
  • line 27: [toDoubleOrNull] accepts only the decimal point. To allow a French user to type [22,5], the comma is replaced with a period. If the input is not a number, [toDoubleOrNull] returns [null]: there is no need to catch an exception as the Java version does with [Double.parseDouble];
  • Line 33: If the entered temperature is correct, it is assigned to the property [temperature] of the emitter [e1], whose accessor [set] signals the event [TemperatureTooHigh] if it is greater than 19 °C.

The results show (times vary from one run to another):

  • line 1: 17 °C does not exceed the threshold: no event;
  • lines 2–6: 21 °C triggers the event. The three listeners are notified in the order in which they were registered. [s1], which accepts only one alert, unsubscribes immediately, but this does not prevent the log, registered after it, from being notified;
  • lines 9–11: [s1] is no longer notified: unsubscription via a new reference worked as intended;
  • lines 12–15: [s0] receives its third alert and unsubscribes in turn;
  • lines 16–17: Only the log remains.

What would happen if there were simply an [ArrayList] instead of the [CopyOnWriteArrayList] in the [Emitter] class? We tested this: when [s1] unsubscribes while the list is being processed, the subsequent items are skipped and the processing stops immediately. The log is not notified of temperatures 21 and 25, with no error message. In other configurations, the loop throws a [ConcurrentModificationException]. A listener list must therefore always support changes while it is being iterated over: the [CopyOnWriteArrayList] is designed for this purpose, and it also supports subscriptions and unsubscriptions made by other threads (Chapter 8).

7.6.3. Property Change Events

The most common type of event occurs when an object’s property changes value: another object (a graphical user interface, a cache, a log) wants to be notified of this change. Kotlin and the Java library provide two complementary tools for this scenario:

  • Kotlin's delegated properties. A property declared as [var p: T by delegate] delegates the reading and writing of its value to another object, the delegate. The standard library provides several of these, such as [by lazy { … }], which calculates the property’s value on the first access. The object [kotlin.properties.Delegates] provides two delegates that accept a lambda: [Delegates.observable(initialValue) { property, old, new -> … }] executes the lambda after each write to the property; [Delegates.vetoable(initialValue) { property, old, new -> … }] executes it beforehand and accepts the new value only if the lambda returns [true]. The parameter [property] is a reference to the property ([KProperty]), whose name is obtained via [property.name];
  • the Java class [java.beans.PropertyChangeSupport], which manages the list of listeners for us. The listeners implement the functional interface [PropertyChangeListener], whose method [void propertyChange(PropertyChangeEvent evt)] receives an event containing the source, the property name, and its old and new values.

The delegate [observable] knows when a property changes, and [PropertyChangeSupport] knows who to notify: the program [Lambdas12.kt] links them together:

package lambdas12

import java.beans.PropertyChangeListener
import java.beans.PropertyChangeSupport
import kotlin.properties.Delegates
import kotlin.properties.ReadWriteProperty

// property change events: Delegates.observable, vetoable and PropertyChangeSupport

fun main() {
    val thermometer = Thermometer("living room")
    // a listener of all the properties
    val log = PropertyChangeListener { e ->
        val source = e.source as Thermometer
        println("Log: ${source.name}.${e.propertyName} : ${e.oldValue} -> ${e.newValue}")
    }
    thermometer.addPropertyChangeListener(log)
    // a listener of the temperature property only
    thermometer.addPropertyChangeListener("temperature") { e ->
        if (e.newValue as Double > 19) {
            println("Alert: temperature too high")
        }
    }

    thermometer.temperature = 18.0
    thermometer.temperature = 21.5
    // same value: no event
    thermometer.temperature = 21.5
    thermometer.name = "lounge"
    // unregistration of the log
    thermometer.removePropertyChangeListener(log)
    thermometer.temperature = 22.0
    println("remaining listeners: ${thermometer.nbListeners}")

    // a property that can refuse a new value
    for (value in listOf(21.0, 45.0)) {
        thermometer.setpoint = value
        println("requested setpoint: $value, retained setpoint: ${thermometer.setpoint}")
    }
}

// a class whose properties signal their changes
class Thermometer(name: String) {
    // listener management is delegated to a PropertyChangeSupport object
    private val changes = PropertyChangeSupport(this)

    // an observable property: after each write, the change is signaled
    private fun <T> observable(initialValue: T): ReadWriteProperty<Any?, T> =
        Delegates.observable(initialValue) { property, old, new ->
            changes.firePropertyChange(property.name, old, new)
        }

    var name: String by observable(name)
    var temperature: Double by observable(0.0)

    // a property that refuses values outside the interval [5, 30]
    var setpoint: Double by Delegates.vetoable(20.0) { _, _, new -> new in 5.0..30.0 }

    // registration and unregistration of the listeners
    fun addPropertyChangeListener(listener: PropertyChangeListener) {
        changes.addPropertyChangeListener(listener)
    }

    fun addPropertyChangeListener(property: String, listener: PropertyChangeListener) {
        changes.addPropertyChangeListener(property, listener)
    }

    fun removePropertyChangeListener(listener: PropertyChangeListener) {
        changes.removePropertyChangeListener(listener)
    }

    val nbListeners: Int
        get() = changes.propertyChangeListeners.size
}

Results:

1
2
3
4
5
6
7
8
9
> gradlew -q run -Pmain=lambdas12.Lambdas12Kt
Log: living room.temperature : 0.0 -> 18.0
Log: living room.temperature : 18.0 -> 21.5
Alert: temperature too high
Log: lounge.name : living room -> lounge
Alert: temperature too high
remaining listeners: 1
requested setpoint: 21.0, retained setpoint: 21.0
requested setpoint: 45.0, retained setpoint: 21.0

Comments:

  • Lines 3–4: The [java.beans] package belongs to the [java.desktop] module of JDK, not to the [java.base] module. Since our program is not a Java module, all classes in JDK are accessible to it: we simply need to import them;
  • Line 45: The [Thermometer] class delegates the management of its listeners to a [PropertyChangeSupport] object, constructed using the event source ([this]). The methods on lines 60–69 simply pass subscription and unsubscription events to it, and the [nbListeners] property (lines 72–73) returns the number of registered listeners;
  • lines 48–50: a small generic function that creates a [Delegates.observable] delegate whose lambda passes each change to [firePropertyChange], along with the property name ([property.name]), the old value, and the new value. It returns a *[ReadWriteProperty<Any?, T>]*, the interface for delegates of mutable properties. Like a method, a lambda written within a class can access the object’s private properties, in this case [changes];
  • lines 53–54: the class’s two observable properties. The keyword [by] delegates their management to a delegate created by our function [observable]. Compared to the Java version, where each setter must store the old value, save the new one, and call *[firePropertyChange] with the property name written as a string, the Kotlin version fits into a single line per property, and the property name cannot be misspelled. The delegate executes the lambda on every write, even if the value doesn’t change; however, [firePropertyChange]* only notifies the listeners if the two values are different: the write on line 28, which doesn’t change the value, therefore produces no output;
  • lines 13–17: a first observer, a lambda converted to [PropertyChangeListener] and stored in a variable so it can be unsubscribed (line 31). It is subscribed to all of the object’s properties. [e.source] (i.e., [e.getSource()], viewed from Kotlin as a property) is of type [Any], which must be cast to [as]. The values are objects: [Double] objects are displayed using their [toString] method, which uses the decimal point;
  • Lines 19–21: A second listener, registered solely for the property [temperature]. The lambda is moved out of the parentheses and converted to [PropertyChangeListener] (conversion SAM). It triggers an alert when the temperature exceeds 19 °C. [e.newValue as Double] converts the new value to the type [Any?];
  • Lines 25–33: property changes are made via simple assignments. The name change (line 29) is reported only to the log. On line 4 of the results, the log displays the new source name, [lounge]: the event is reported after the change. After unsubscribing from the log, only the alert is displayed, and only one listener remains;
  • line 57: the property [setpoint] uses the delegate [vetoable]. Its lambda, called before each write operation, accepts the new value only if it is between 5 and 30 °C ([new in 5.0..30.0]); its first two parameters, which are unnecessary here, are denoted as [_]. Lines 36–38: the value 21 is accepted, while the value 45 is rejected; the property retains its previous value (lines 7–8 of the results). The JavaBeans instances have an equivalent mechanism, with [VetoableChangeSupport] and listeners that throw a [PropertyVetoException].

Without writing an event class or a listener type, we thus obtain a complete mechanism. [PropertyChangeSupport] is the one used by Swing GUI components and JavaBeans; JavaFX has its own, very similar mechanism for observable properties. The delegates [observable] and [vetoable], on the other hand, are sufficient on their own when there is only one observer, known at the time the class is written. Finally, note that for asynchronous event streams, Kotlin provides the [Flow] from the kotlinx.coroutines library, which we’ll cover in Chapter 9 ([StateFlow], in particular, acts as an observable property).

7.7. Lambdas, Collections, and Sequences

In Chapter 5, we used the functional operations of collections ([filter], [map], [sortedBy]…) and sequences by passing lambdas to them. We now know what these parameters are: the function [filter] is declared as follows in the standard library: [inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T>].

It is an extension function, [inline], whose parameter is a functional type: any function or lambda that takes an element and returns a Boolean is suitable. Similarly, [map] expects a [(T) -> R], [sortedBy] expects a [(T) -> R?], and [R] is comparable, [forEach] corresponds to a [(T) -> Unit], [reduce] to a [(S, T) -> S]… To fully understand how they work, let’s write our own filtering and transformation operations. Here is the program [Lambdas13.kt]:

package lambdas13

// lambdas, collections and sequences

fun main() {
    val numbers = listOf(3, 8, 1, 6, 5, 4, 9, 2)

    // 1 - filter and map expect functions (T) -> Boolean and (T) -> R
    val isEven: (Int) -> Boolean = { it % 2 == 0 }
    val square: (Int) -> Int = { it * it }
    println("collections: ${numbers.filter(isEven).map(square)}")
    // the Java Stream API remains usable: the lambdas are converted to Predicate and Function
    println("Stream: ${numbers.stream().filter { it % 2 == 0 }.map { it * it }.toList()}")

    // 2 - our own filtering function, called with a lambda then a reference
    println("filterBy(n > 4): ${numbers.filterBy { it > 4 }}")
    println("filterBy(isOdd): ${numbers.filterBy(::isOdd)}")

    // 3 - a collection is evaluated immediately: each step produces a complete list
    val largeList = numbers.filter {
        print("[test $it] ")
        it > 7
    }
    println()
    println("the list is already computed: $largeList")

    // 4 - a sequence is evaluated on demand:
    //     the predicate is only called by the terminal operation
    val large = numbers.asSequence().filter {
        print("[test $it] ")
        it > 7
    }
    println("the sequence is defined, nothing has been executed yet")
    large.forEach { print("-> $it ") }
    println()

    // 5 - our own intermediate operations on sequences, lazy as well
    val cities = listOf("Angers", "Paris", "Lyon", "Nantes", "Aix")
    val result = cities.asSequence()
        .keep { it.length <= 5 }
        .transform(String::uppercase)
        .toList()
    println("our operations: $result")
    // an infinite sequence: only the requested elements are computed
    val powersOfTwo = generateSequence(1) { it * 2 }
    val firstOnes = powersOfTwo.transform { "2^${it.countTrailingZeroBits()}=$it" }.take(5)
    println("powers of 2: ${firstOnes.toList()}")
}

// filters a list with a predicate: the work that filter does
inline fun <T> List<T>.filterBy(predicate: (T) -> Boolean): List<T> {
    val result = mutableListOf<T>()
    for (element in this) {
        if (predicate(element)) {
            result.add(element)
        }
    }
    return result
}

fun isOdd(n: Int) = n % 2 != 0

// an intermediate sequence operation that lets through only the elements satisfying the predicate
fun <T> Sequence<T>.keep(predicate: (T) -> Boolean): Sequence<T> = sequence {
    for (element in this@keep) {
        if (predicate(element)) yield(element)
    }
}

// an intermediate operation that transforms each element: the equivalent of map
fun <T, R> Sequence<T>.transform(transformation: (T) -> R): Sequence<R> = sequence {
    for (element in this@transform) {
        yield(transformation(element))
    }
}

Results:

> gradlew -q run -Pmain=lambdas13.Lambdas13Kt
collections: [64, 36, 16, 4]
Stream: [64, 36, 16, 4]
filterBy(n > 4): [8, 6, 5, 9]
filterBy(isOdd): [3, 1, 5, 9]
[test 3] [test 8] [test 1] [test 6] [test 5] [test 4] [test 9] [test 2]
the list is already computed: [8, 9]
the sequence is defined, nothing has been executed yet
[test 3] [test 8] -> 8 [test 1] [test 6] [test 5] [test 4] [test 9] -> 9 [test 2]
our operations: [PARIS, LYON, AIX]
powers of 2: [2^0=1, 2^1=2, 2^2=4, 2^3=8, 2^4=16]

Comments:

  • Lines 9–11: The lambdas from [filter] and [map] are first assigned to variables to reveal their types. [map] transforms each element: this yields the squares of even numbers;
  • line 13: the same calculation using the Java API stream, which can be used as-is from Kotlin: the Java method [filter] expects a [Predicate], [map] expects a [Function], and Kotlin lambdas are automatically converted (SAM conversion). It is rarely used in Kotlin for sequential computations, since collection and sequence functions are simpler; it remains useful for the parallel streams in Chapter 8, or when a Java library returns a [Stream];
  • Lines 51–58: Our function [filterBy], written as an extension function of [List<T>] and declared as [inline], just like [filter]. It is generic: it filters a list of elements of any type [T]. It iterates over the list ([this]), calls the predicate on each element (line 54), and returns the list of selected elements;
  • Line 16: call to [filterBy] with a lambda, outside the parentheses. The type [T] is inferred from the list: [Int]. On line 17, a function reference is passed to it: [isOdd] (line 61) has the form [(Int) -> Boolean];
  • Lines 20–25: a verbose predicate, which displays each element it tests, applied to a list. Collection operations are immediate: [filter] tests all elements and constructs the complete result list before returning it (lines 5–6 of the results). In a chain [list.filter { … }.map { … }], each step thus creates an intermediate list;
  • lines 29–34: the same predicate applied to a sequence ([asSequence()]). The result (lines 7–8) shows that the sequence definition does not execute anything: the message on line 33 is displayed before the first test. It is only when the terminal operation [forEach] requests the elements that the predicate executes, and the tests alternate with the displays from [forEach]: each element passes through the entire chain before the next one enters it. This is the deferred (or lazy) execution of sequences, the same as that of Java streams. The intermediate operations of the sequences are not [inline]: they must retain the lambdas in order to execute them later;
  • lines 64–73: To write our own intermediate operations—which are lazy, like those in the library—we use the sequence constructor [sequence]. It takes a lambda in which the function [yield] produces an element of the sequence: for each element requested by the downstream, the lambda resumes execution where it left off, until the next [yield] (this is a coroutine, a concept we will study in Chapter 9). The label of [this@keep] designates the receiver of the extension function [keep], that is, the original sequence: without a label, [this] would denote the receiver of the lambda in [sequence], since that lambda is a lambda with a receiver. The function [keep] passes only those elements that satisfy the predicate; the function [transform], which is doubly generic ([T] is the type of the received elements, [R] is the type of the produced elements), returns the result of the function applied to each element. Java 24 introduced intermediate collectors ([Gatherer]) for the same purpose, which have a significantly more complex syntax;
  • lines 39–42: Our extension functions are inserted into the string just like library operations: we keep cities with at most 5 letters, then convert them to uppercase. The terminal operation [toList] triggers the calculation;
  • lines 45–47: [generateSequence(1) { it * 2 }] defines an infinite sequence: 1, followed by each element obtained by doubling the previous one. It is usable only thanks to lazy evaluation: [take(5)] requests only the first five elements, and only those are computed. The lambda passed to [transform] displays each power in the form [2^n=value] ([countTrailingZeroBits] counts the rightmost zeros in the binary representation, i.e., the exponent).

All operations on collections and sequences are built on this model: higher-order functions that take functional types. For collections, these functions are [inline] and immediate; for sequences, they preserve the lambdas and compute the elements only when requested by the terminal operation.

7.8. Expression Trees

Some languages, such as C#, allow a lambda to be converted not into executable code, but into a data structure that describes it (an “expression tree”): a program can then parse the lambda [t => t.Limit > 20000] and translate it, for example, into a clause SQL [WHERE limits > 20000] executed by the database server.

Kotlin, like Java, has no equivalent: a Kotlin lambda is always compiled into executable code; it can be called, but its internal workings cannot be examined. Several tools come close, however:

  • Property references: [Person::age] is an object ([KProperty1<Person, Int>]) that knows the property name ([Person::age.name]) and its type. A library can use this to construct a query without writing a string: this is what Spring Data MongoDB does, for example, with its Kotlin extensions, which allow you to write a criterion such as [Person::age gt 18];
  • Internal DSL (“Domain-Specific Languages”): thanks to lambdas with receivers, [infix] functions, and operator overloading, a Kotlin library can offer a syntax similar to SQL, which the program executes to construct a data structure describing the query. This is the principle behind the Exposed library, from JetBrains: [Brackets.selectAll().where { Brackets.limit greater 20000 }];
  • compiler plugins: the Kotlin compiler can be extended with plugins that transform the code. This is how kotlinx.serialization works, the [all-open] and [no-arg] plugins used with Spring and Hibernate, or the [power-assert] plugin, which analyzes an assertion’s expression to display its details in case of failure. However, these are development tools, not built-in features of the program.

Hibernate, which we will cover in Chapter 11, offers the same solutions as in Java: a query language similar to SQL but expressed in terms of classes and properties, HQL (["from Bracket t where t.limit > 20000"]), and Jakarta Persistence’ API Criteria, which constructs the query through method calls ([cb.gt(t.get("limit"), 20000)]). In both cases, the query is data (a string or an object tree) that Hibernate translates into SQL, whereas a lambda passed to [filter] is executed within the Kotlin program.

7.9. Conclusion

Key takeaways from this chapter:

  • In Kotlin, functions are values: they have a functional type ([(Int, Int) -> Int], [() -> Unit], [(T) -> Boolean], [(() -> Unit)?], [T.() -> R]…), can be stored in variables, passed as parameters, returned as results, and called like ordinary functions. To the virtual machine, they are objects that implement the interfaces [Function0], [Function1]…;
  • the interfaces of [java.util.function] ([Function], [Supplier], [Consumer], [Predicate]…), along with [Runnable], [Callable], and [Comparator], remain usable: when a Java method expects a functional interface, Kotlin automatically converts a lambda or a reference to it (conversion SAM); elsewhere, the conversion is written explicitly: [Comparator { … }];
  • to name a function type, we have the alias [typealias] (simply an alias) and [fun interface] (a true new type, implementable via a lambda);
  • A function can be obtained via a lambda expression ([{ a, b -> a + b }], [{ it * it }]—written outside parentheses when it is the last parameter, and closed with [return@label]), via an anonymous function, via an anonymous object, or via a reference ([::function], [object::method], [Class::method], [::Class]). Two references to the same function of the same object are equal; two lambdas are never equal;
  • lambdas capture the variables surrounding them, including the [var], which they can modify (the compiler stores them in [Ref] objects); they then see the value of the variable at the time of their execution, which requires caution in loops and between threads;
  • Functions that return functions and extensions on functional types ([then], [and], [or]…) compose new functions; [compareBy], [thenBy], and [reversed] construct comparators;
  • the functions [inline] copy their code and that of the lambdas to the call site, without an object or conversion, and allow non-local returns; [noinline] and [crossinline] handle special cases. The scope functions [let], [run], [with], [apply], and [also] are the most common applications of this;
  • a functional type is chosen for short-term, one-off behavior; a [fun interface] for a distinct type; or a API that can be used from Java; a multi-member interface (or a “business” interface) for an application role;
  • an event is set up using the listener pattern: an [XxxEvent] class, a listener type (often a functional-type [typealias]), [addXxxListener] / [removeXxxListener], and a list of functions ([CopyOnWriteArrayList]) iterated over by a [fireXxx] method. The delegated properties [Delegates.observable] and [Delegates.vetoable] signal or filter changes to a property, and [PropertyChangeSupport] manages the list of listeners for them;
  • collection operations (immediate, [inline]) and sequence operations (lazy) are higher-order functions; Kotlin does not have expression trees, and Hibernate expresses its queries using HQL or the Criteria API.

Lambdas will reappear in all subsequent chapters: a thread executes a lambda ([thread { … }]), and an [ExecutorService] executes a [Callable] (Chapter 8); a coroutine is launched by [launch { … }] or [async { … }] (Chapter 9), extension functions of [ResultSet] will receive, in the form of a lambda, the transformation of a line from the result into an object (Chapter 10), and Spring Boot web services process HTTP requests in methods that the framework calls as listeners (Chapter 13).