Lambda expressions
This is a chapter from the book Functional Kotlin. You can find it on LeanPub or Amazon. It is also available as a course.
Lambda expressions are a shorter alternative to anonymous functions. They are also used to define objects that represent functions. Both notations compile to the same result, but lambda expressions support more features (most of which will be presented in this chapter). In the end, lambda expressions are the most popular and idiomatic approach to create objects that represent functions, therefore understanding them is essential for using Kotlin’s functional programming features.
An expression used to create an object representing a function is called a function literal, so both lambda expressions and anonymous functions are function literals.
Tricky braces
Lambda expressions are defined in braces (curly brackets). What is more, even just empty braces define a lambda expression.
But be careful because all braces that are not part of a Kotlin structure are lambda expressions (we can call them orphaned lambda expressions). This can lead to a lot of problems. Take a look at the following example: What does the following main
function print?
The answer is nothing. It creates a lambda expression that is never invoked. Another question: What does the following produce
function return?
Counterintuitively, it is not 42
. Braces are not a part of single-expression function notation. The produce
function returns a lambda expression of type () -> Int
, so the above code on JVM should print something like Function0<java.lang.Integer>
, or just () -> Int
. To fix this code, we should either call the produced function or remove the braces inside the single-expression function definition.
Parameters
If a lambda expression has parameters, we need to separate the content of the braces with an arrow ->
. Before the arrow, we specify parameter names and types, separated by commas. After the arrow, we specify the function body.
Most often, we define lambda expressions as arguments to some functions. Regular functions need to define their parameter types, based on which lambda expression parameter types can be inferred.
If we want to ignore a parameter, we can use underscore (_
) instead of its name. This is a placeholder that shows that this parameter is ignored.
IDEA IntelliJ suggests transforming unused parameters into underscores.
We can also use destructuring when defining a lambda expression’s parameters1.
Trailing lambdas
Kotlin introduced a convention: if we call a function whose last parameter is of a functional type, we can define a lambda expression outside the parentheses. This feature is known as trailing lambda. If it is the only argument we define, we can skip the parameter bracket and just define a lambda expression. Take a look at these examples.
In the example above, both
run
andrepeat
are simplified functions from the standard library.
This means that we can call our setOnClickListener
in the following way:
Remember sum
and product
from the introduction? We have implemented them using the fold
function with a trailing lambda.
But be careful because this convention works only for the last parameter. Take a look at the snippet below and guess what will be printed.
The answer is "CAAB". Tricky, isn't it? If you call a function with more than one functional parameter, use the named argument convention2.
Result values
Lambda expressions were initially designed to implement short functions. Their bodies were designed to be minimalistic; therefore, inside them, instead of using an explicit return
, the result of the last statement is returned. For example, { 42 }
returns 42
because this number is the last statement. { 1; 2 }
returns 2
. { 1; 2; 3 }
returns 3
.
In most use cases, this is really convenient, but what can we do if we need to finish our function prematurely? A simple return
will not help (for reasons we will cover later).
To use return
in the middle of a lambda expression, we need to use a label that marks this lambda expression. We specify a label before a lambda expression by using the label name followed by @
. Then, we can return from this lambda expression calling return
on the defined label.
To simplify this process, there is a convention: if a lambda expression is used as an argument to a function, the name of this function becomes its default label. So, without specifying a label, we could return from the lambda using the onUserChanged
label in the example above.
This is how we typically return from a lambda expression prematurely. In theory, specifying custom labels might be useful for returning from outer lambda expressions.
However, in practice, this is not only rare but also considered a poor practice3, because it violates the usual encapsulation rules. This is similar to throwing an exception from an inner function, but in this case the caller can at least decide to catch and react. However, returning from an outer label completely ignores the intermediate callers.
Lambda expression examples
The previous chapter showed a set of functions implemented with anonymous functions. This is how they might be defined with lambda expressions:
A lambda expression can specify the types of parameters, so the result type can be inferred:
On the other hand, when parameter types can be inferred, lambda expressions do not need to define them:
An implicit name for a single parameter
When a lambda expression has exactly one parameter, we can reference it using the it
keyword instead of specifying its name. Since the type of it
cannot be specified explicitly, it needs to be inferred. Despite this, it is still a very popular feature.
Closures
A lambda expression can use and modify variables from the scope where it is defined.
A lambda expression that refers to an object defined outside its scope, like the lambda expression in the above example that refers to the local variable i
, is called a closure.
Lambda expressions vs anonymous functions
Let's compare lambda expressions to anonymous functions. They are both function literals, i.e., structures that create an object representing a function. Under the hood, their efficiency is the same. So, when should we choose one over the other? Take a look at the processor
variable below, which is defined using both approaches.
Lambda expressions are shorter but also less explicit. They return the last expression without an explicit return
keyword. To use return
we need to have a label.
Anonymous functions are longer, but it is clear that they define a function. They use an explicit return
and must specify the result type.
Lambda expressions were mainly designed for single-expression functions, and the documentation suggests using anonymous functions for longer bodies. Although developers used to use lambda expressions practically everywhere, nowadays anonymous functions seem nearly forgotten.
The popularity of lambda expressions is supported by the additional features: trailing lambda, an implicit name for a single parameter, and non-local return (this will be explained later). So, I understand if you decide to forget about anonymous functions and use lambda expressions everywhere. Many developers have already done this.
However, before we close this discussion, we must introduce one more approach for creating objects representing functions. This will be a serious competitor to lambda expressions because it is shorter and has a good-looking, functional style. Let's talk about function references.
More about destructuring in Kotlin Essentials, Data modifier chapter.
Best practices regarding naming arguments are explained in Effective Kotlin, Item 17: Consider naming arguments. The named argument convention is explained in Kotlin Essentials, Functions chapter.
Also, the above algorithm is poorly implemented. It should instead use sumOf
function, which we will present later in this book.