Function references
This is a chapter from the book Functional Kotlin. You can find it on LeanPub or Amazon.
When we need a function as an object, we can create it with a lambda expression, but we can also reference an existing function. The second approach is often shorter and more convenient. In this chapter, we will learn about the different kinds of function references, and we will see how they might be used in practice.
In our examples, we will reference the functions from the following code. These will be the basic functions in this chapter.
Top-level functions references
We use ::
and a function name to reference a top-level function0. Function references are part of the Kotlin reflection API and support introspection. If you include the kotlin-reflect
dependency in your project, you can use a function reference to check if the referenced function has the open
modifier, what annotation it has, etc.1
However, function references also implement function types and can be used as function literals. Such usages are not considered "real" reflection and introduce no performance overhead compared to lambda expressions2.
Notice that add
is a function with two parameters of type Int
, and result type Int
, so its reference function type is (Int, Int) -> Int
.
Let's get back to our basic functions. Can you guess what the function type of zeroComplex
and makeComplex
should be?
A function type specifies the parameters and the result type. The function zeroComplex
has no parameters, and its result type is Complex
, so the function type of its function reference is () -> Complex
. The function makeComplex
has two parameters of type Double
, and its result type is Complex
, so the function type of its function reference is (Double, Double) -> Complex
.
Since the function makeComplex
has default arguments for its parameters, it should also implement (Double) -> Complex
and () -> Complex
. Limited support for such behavior was introduced in Kotlin 1.4, but a reference must still be used as an argument.
Method references
When you reference a method, you need to start with a type, followed by ::
and the method name. Every method needs a receiver, namely the object on which the function should be called. Function references expect it as the first parameter. Take a look at the example below.
The toFloat
function has no explicit parameters, but its function reference requires a receiver of type Number
. The times
function has only one explicit parameter of type Int
, but it also requires another one for the receiver.
Do you remember sum
and product
from the introduction? We implemented them using lambda expressions, but we could also have used method references.
Getting back to our basic functions, can you deduce the function type of Complex::doubled
and Complex::times
?
doubled
has no explicit parameters, a receiver of type Complex
, and the result type is Complex
; therefore, the function type of its function reference is (Complex) -> Complex
. times
has an explicit parameter of type Int
, a receiver of type Complex
, and the result type is Complex
; therefore, the function type of its function reference is (Complex, Int) -> Complex
.
Extension function references
We can reference extension functions in the same way as member functions. Their function types are also analogous.
Can you now guess the function type of Complex::plus
and Int::toComplex
from our basic functions?
plus
has a Complex
parameter, a receiver of type Complex
, and it returns Complex
; therefore, the function type of its function reference is (Complex, Complex) -> Complex
. The toComplex
function has no parameters, a receiver of type Int
, and it returns Complex
; therefore, the function type of its function reference is (Int) -> Complex
.
Method references and generic types
We reference a method on a type, not a property. So, if you want to reference sum
, which is an extension function on the type List<Int>
, you need to use List<Int>::sum
. If you want to reference isNullOrBlank
, which is an extension property on the type String?
, you should use String?::isNullOrBlank
3.
When you reference a method from a generic class, its type arguments need to be explicit. So, in the example below, to reference the unbox
method, we need to use Box<String>::unbox
, and the Box::unbox
notation is not acceptable.
Bounded function references
We have learned how to reference a method on a type, but there is also another option: we can reference a method on an object instance. Such references are called bounded function references.
Notice that the function type of num::toFloat
is () -> Float
in the example above. We have previously learned that the function type of Number::toFloat
is (Number) -> Float
; therefore, in the regular method reference notation, the receiver type will be in the first position. In bounded function references, the receiver object is already provided in the reference, so there is no need to specify it additionally.
Getting back to our basic functions, can you deduce the type of the bounded references to doubled
, times
, plus
, and toComplex
? The answers can be found in the code below.
Bounded function references also work on object expressions and object declarations4.
I find bounded function references especially useful when using libraries like RxJava or Reactor, where we often set handlers for different kinds of events. Small, simple handlers can be defined using lambda expressions. However, extracting them as member functions and setting bounded function references as handlers is a good idea for larger and more complicated handlers.
Using the bounded function reference is really convenient in this case because handlers need to have access to the MainPresenter
properties, but getAllCharacters
should not know anything about this.
A bounded function reference on the receiver (this
) can be used implicitly, so this::show
can also be replaced with ::show
.
Constructor references
A constructor is also considered a function in Kotlin. We call and reference it in the same way as all other functions. This means that to reference the Complex
class constructor, we need to use ::Complex
. The constructor reference has the same parameters as the constructor it references, and its result type is the type of the class whose constructor it is.
I find constructor references useful when I map elements from one type to another using a constructor. This could be especially useful for mapping to wrapper classes. However, mapping using a constructor should not be used too often as we prefer factory functions (like conversion functions) instead of secondary constructors5.
Bounded object declaration references
One of the motivations for the introduction of bounded function references was to make a simple way to reference object declaration methods6. Every object declaration is a singleton, so its name serves as the only object reference. Thanks to the bounded function reference feature, we can reference object declaration methods using its name, followed by two colons (::
), then the method name.
Companion objects are also a form of object declaration. However, referencing their methods using the class name is not enough. We need to use the real companion name, which is Companion
by default.
Function overloading and references
Kotlin allows function overloading, which means defining multiple functions with the same name. During compilation, the Kotlin compiler decides which function should be used based on the types of arguments used.
The same logic is used when we use function references. The compiler determines which function should be chosen based on the expected type. Without a specified type, our code will not compile due to ambiguity.
Therefore, when we eliminate ambiguity with a type, everything will be correctly determined and resolved.
The same is true when we have multiple constructors.
Property references
A property can be considered as a getter or as a getter and a setter. That is why its reference implements the getter function type.
For var
, you can reference the setter using the setter
property from the property reference, but this requires kotlin-reflect
; therefore, I recommend avoiding this approach because it might impact your code’s performance.
There are many kinds of references. Some developers like using them, while others avoid them. Anyway, it is good to know how function references look and behave. It is worth practicing them as they can help make our code more elegant in applications where functional programming concepts are widely used.
Top-level function is a function defined outside a class, so in a file.
More about reflection in Advanced Kotlin, Reflection chapter.
For this, the reference needs to be immediately typed as a function type.
It is possible to reference this function by String::isNullOrBlank
, but such reference function type is (String) -> Boolean
, makes it not accept null
and effectively behave like String::isBlank
.
More about object expressions and object declarations in Kotlin Essentials, Objects chapter.
See Effective Kotlin, Item 32: Consider factory functions instead of secondary constructors.