article banner

Function references

This is a chapter from the book Functional Kotlin. You can find it on LeanPub or Amazon. It is also available as a course.

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.

data class Complex(val real: Double, val imaginary: Double) { fun doubled(): Complex = Complex(this.real * 2, this.imaginary * 2) fun times(num: Int) = Complex(real * num, imaginary * num) } fun zeroComplex(): Complex = Complex(0.0, 0.0) fun makeComplex( real: Double = 0.0, imaginary: Double = 0.0 ) = Complex(real, imaginary) fun Complex.plus(other: Complex): Complex = Complex(real + other.real, imaginary + other.imaginary) fun Int.toComplex() = Complex(this.toDouble(), 0.0)

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

fun add(a: Int, b: Int) = a + b fun main() { val f = ::add // function reference println(f.isOpen) // false println(f.visibility) // PUBLIC // The above statements require `kotlin-reflect` // dependency }

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.

fun add(a: Int, b: Int) = a + b fun main() { val f: (Int, Int) -> Int = ::add // an alternative to: // val f: (Int, Int) -> Int = { a, b -> add(a, b) } println(f(10, 20)) // 30 }

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.

fun zeroComplex(): Complex = Complex(0.0, 0.0) fun makeComplex( real: Double = 0.0, imaginary: Double = 0.0 ) = Complex(real, imaginary) data class Complex(val real: Double, val imaginary: Double) fun main() { val f1: () -> Complex = ::zeroComplex println(f1()) // Complex(real=0.0, imaginary=0.0) val f2: (Double, Double) -> Complex = ::makeComplex println(f2(1.0, 2.0)) // Complex(real=1.0, imaginary=2.0) }

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.

fun produceComplex1(producer: ()->Complex) {} produceComplex1(::makeComplex) fun produceComplex2(producer: (Double)->Complex) {} produceComplex2(::makeComplex)

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.

data class Number(val num: Int) { fun toFloat(): Float = num.toFloat() fun times(n: Int): Number = Number(num * n) } fun main() { val numberObject = Number(10) // member function reference val float: (Number) -> Float = Number::toFloat // `toFloat` has no parameters, but its function type // needs a receiver of type `Number` println(float(numberObject)) // 10.0 val multiply: (Number, Int) -> Number = Number::times println(multiply(numberObject, 4)) // Number(num = 40.0) // `times` has one parameter of type `Int`, but its // function type also needs a receiver of type `Number` }

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.

fun sum(a: Int, b: Int) = (a..b).fold(0, Int::plus) fun product(a: Int, b: Int) = (a..b).fold(1, Int::times)

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.

data class Complex(val real: Double, val imaginary: Double) { fun doubled(): Complex = Complex(this.real * 2, this.imaginary * 2) fun times(num: Int) = Complex(real * num, imaginary * num) } fun main() { val c1 = Complex(1.0, 2.0) val f1: (Complex) -> Complex = Complex::doubled println(f1(c1)) // Complex(real=2.0, imaginary=4.0) val f2: (Complex, Int) -> Complex = Complex::times println(f2(c1, 4)) // Complex(real=4.0, imaginary=8.0) }

Extension function references

We can reference extension functions in the same way as member functions. Their function types are also analogous.

data class Number(val num: Int) fun Number.toFloat(): Float = num.toFloat() fun Number.times(n: Int): Number = Number(num * n) fun main() { val num = Number(10) // extension function reference val float: (Number) -> Float = Number::toFloat println(float(num)) // 10.0 val multiply: (Number, Int) -> Number = Number::times println(multiply(num, 4)) // Number(num = 40.0) }

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.

data class Complex(val real: Double, val imaginary: Double) fun Complex.plus(other: Complex): Complex = Complex(real + other.real, imaginary + other.imaginary) fun Int.toComplex() = Complex(this.toDouble(), 0.0) fun main() { val c1 = Complex(1.0, 2.0) val c2 = Complex(4.0, 5.0) // extension function reference val f1: (Complex, Complex) -> Complex = Complex::plus println(f1(c1, c2)) // Complex(real=5.0, imaginary=7.0) val f2: (Complex, Int) -> Complex = Complex::times println(f2(c1, 4)) // Complex(real=4.0, imaginary=8.0) }

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?::isNullOrBlank3.

class TeamPoints(val points: List<Int>) { fun <T> calculatePoints(operation: (List<Int>) -> T): T = operation(points) } fun main() { val teamPoints = TeamPoints(listOf(1, 3, 5)) val sum = teamPoints .calculatePoints(List<Int>::sum) println(sum) // 9 val avg = teamPoints .calculatePoints(List<Int>::average) println(avg) // 3.0 val invalid = String?::isNullOrBlank println(invalid(null)) // true println(invalid(" ")) // true println(invalid("AAA")) // false }

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.

class Box<T>(private val value: T) { fun unbox(): T = value } fun main() { val unbox = Box<String>::unbox val box = Box("AAA") println(unbox(box)) // AAA }

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.

data class Number(val num: Int) { fun toFloat(): Float = num.toFloat() fun times(n: Int): Number = Number(num * n) } fun main() { val num = Number(10) // bounded function reference val getNumAsFloat: () -> Float = num::toFloat // There is no need for receiver type in function type, // because reference is already bound to an object println(getNumAsFloat()) // 10.0 val multiplyNum: (Int) -> Number = num::times println(multiplyNum(4)) // Number(num = 40.0) }

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.

data class Complex(val real: Double, val imaginary: Double) { fun doubled(): Complex = Complex(this.real * 2, this.imaginary * 2) fun times(num: Int) = Complex(real * num, imaginary * num) } fun Complex.plus(other: Complex): Complex = Complex(real + other.real, imaginary + other.imaginary) fun Int.toComplex() = Complex(this.toDouble(), 0.0) fun main() { val c1 = Complex(1.0, 2.0) val f1: () -> Complex = c1::doubled println(f1()) // Complex(real=2.0, imaginary=4.0) val f2: (Int) -> Complex = c1::times println(f2(17)) // Complex(real=17.0, imaginary=34.0) val f3: (Complex) -> Complex = c1::plus println(f3(Complex(12.0, 13.0))) // Complex(real=13.0, imaginary=15.0) val f4: () -> Complex = 42::toComplex println(f4()) // Complex(real=42.0, imaginary=0.0) }

Bounded function references also work on object expressions and object declarations4.

object SuperUser { fun getId() = 0 } fun main() { val myId = SuperUser::getId println(myId()) // 0 val obj = object { fun cheer() { println("Hello") } } val f = obj::cheer f() // Hello }

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.

class MainPresenter( private val view: MainView, private val repository: MarvelRepository ) : BasePresenter() { fun onViewCreated() { subscriptions += repository.getAllCharacters() .applySchedulers() .subscribeBy( onSuccess = this::show, onError = view::showError ) } fun show(items: List<MarvelCharacter>) { // ... view.show(items) } }

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.

data class Complex(val real: Double, val imaginary: Double) fun main() { // constructor reference val produce: (Double, Double) -> Complex = ::Complex println(produce(1.0, 2.0)) // Complex(real=1.0, imaginary=2.0) }

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.

class StudentId(val value: Int) class UserId(val value: Int) { constructor(studentId: StudentId) : this(studentId.value) } fun main() { val ints: List<Int> = listOf(1, 1, 2, 3, 5, 8) val studentIds: List<StudentId> = ints.map(::StudentId) val userIds: List<UserId> = studentIds.map(::UserId) }

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.

object Robot { fun moveForward() { /*...*/ } fun moveBackward() { /*...*/ } } fun main() { Robot.moveForward() Robot.moveBackward() val action1: () -> Unit = Robot::moveForward val action2: () -> Unit = Robot::moveBackward }

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.

class Drone { fun setOff() {} fun land() {} companion object { fun makeDrone(): Drone = Drone() } } fun main() { val maker: () -> Drone = Drone.Companion::makeDrone }

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.

fun foo(i: Int) = 1 fun foo(str: String) = "AAA" fun main() { println(foo(123)) // 1 println(foo("")) // AAA }

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.

fun foo(i: Int) = 1 fun foo(str: String) = "AAA" fun main() { val fooInt: (Int) -> Int = ::foo println(fooInt(123)) // 1 val fooStr: (String) -> String = ::foo println(fooStr("")) // AAA }

The same is true when we have multiple constructors.

class StudentId(val value: Int) data class UserId(val value: Int) { constructor(studentId: StudentId) : this(studentId.value) } fun main() { val intToUserId: (Int) -> UserId = ::UserId println(intToUserId(1)) // UserId(value=1) val studentId = StudentId(2) val studentIdToUserId: (StudentId) -> UserId = ::UserId println(studentIdToUserId(studentId)) // UserId(value=2) }

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.

data class Complex(val real: Double, val imaginary: Double) fun main() { val c1 = Complex(1.0, 2.0) val c2 = Complex(3.0, 4.0) // property reference val getter: (Complex) -> Double = Complex::real println(getter(c1)) // 1.0 println(getter(c2)) // 3.0 // bounded property reference val c1ImgGetter: () -> Double = c1::imaginary println(c1ImgGetter()) // 2.0 }

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.

0:

Top-level function is a function defined outside a class, so in a file.

1:

More about reflection in Advanced Kotlin, Reflection chapter.

2:

For this, the reference needs to be immediately typed as a function type.

3:

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.

4:

More about object expressions and object declarations in Kotlin Essentials, Objects chapter.

5:

See Effective Kotlin, Item 32: Consider factory functions instead of secondary constructors.

6:

For details, see KEEP, link: kt.academy/l/keep-bound-ref