Author profile picture

Function Reference

Overview

Kotlin supports the notion of first-class functions - functions can be saved to variables, passed into functions, and returned from functions. When you need to do this, you can create lambdas - function literals - or you can refer to an existing function by means of a function reference.

Usage

Assigning a function:

class Example {
	fun add(a: Int, b: Int) = a + b
	val operation = this::add
}

Passing a function:

class Example {
	fun add(a: Int, b: Int) = a + b
	fun mlt(a: Int, b: Int) = a * b
	
	fun doIt(operation: (Int, Int) -> Int) {
		operation.invoke(2, 3)
	}

	fun doAdd() = doIt(this::add)
	fun doMultiply() = doIt(this::mlt)
}

Returning a function:

class Example {
	fun add(a: Int, b: Int) = a + b
	fun mlt(a: Int, b: Int) = a * b
	
	fun getOperation(name: String) = when(name) {
		"addition" -> this::add
		"multiplication" -> this::mlt
		else -> { a, b -> 0 }
	}
}

Bound and Unbound References

Function references can be either bound or unbound. A bound function reference is associated with an instance, whereas an unbound function reference will need the instance provided when it’s invoked. To help understand the difference, consider this example:

class Book(val contentPageCount: Int, val introductionPageCount: Int) {
    fun calculatePages() = contentPageCount + introductionPageCount
}

fun main(args: Array<String>) {
    val warAndPeace = Book(1440, 10)
    val returnOfSherlockHolmes = Book(304, 5)

    // Unbound reference
    val calculatePages = Book::calculatePages
    val warAndPeacePageCount = calculatePages(warAndPeace)

    // Bound reference
    val calculatePagesOfSherlock = returnOfSherlockHolmes::calculatePages
    val sherlockPageCount = calculatePagesOfSherlock()
}

Notice that for this unbound reference, you have to pass the instance. The resulting operation, calculatePages(warAndPeace), is effectively the same as warAndPeace.calculatePages(). The first is an expression from the perspective of a function, whereas the second is an expression from the perspective of an object.

Unbound Function References

Some unbound function references aren’t associated with a class, and so they won’t need any additional arguments. For example, if you’ve got a top-level function, then the function reference does not need to be qualified.

fun add(a: Int, b: Int) = a + b

class Example {
	val operation = ::add
	val sum = operation(1,2)
}

Because it’s a top-level function, operation only requires the arguments that add requires. Note the difference compared to calculatePages in the previous example, where the arity - that is, the number of arguments - of the reference was greater (1) than the arity of the function that it referenced (0).

Local function references are also unbound, and do not need to be qualified on the left-hand side:

class Example {
    fun assignSomething() {
        fun add(a: Int, b: Int) = a + b
        val operation = ::add
        val sum = operation(1,2)
    }
}

Bound References

As demonstrated above, a bound function reference is associated with an instance. So, when referencing a function that’s part of an object, it’ll need to be qualified with the object reference:

class Book(val contentPageCount: Int, val introductionPageCount: Int) {
    fun calculatePages() = contentPageCount + introductionPageCount
}

fun main(args: Array<String>) {
    val returnOfSherlockHolmes = Book(304, 5)

    // Bound reference
    val calculatePages = returnOfSherlockHolmes::calculatePages
    val pageCount = calculatePages()
}

The receiver of the reference - that is, the left-hand side - doesn’t have to be a variable; it can be any expression! The expression is evaluated up front and won’t change over time, even if the elements of the expression change. For example:

var favoriteNumber = 5
val toStringReference = (favoriteNumber + 12)::toString

// toStringReference() == "17"

favoriteNumber += 1

// toStringReference() == "17"

Bound function references were introduced in Kotlin 1.1

Alternatives

A function reference is typically used to pass an expression or set of instructions from one part of your code to another. There are a few other ways to accomplish this in Kotlin.

Lambdas

Lambdas - function literals - are the typical choice for passing a function around. If the function is small (single expressions are the best), and only needs to be used in one place, a lambda is a fantastic choice.

A function reference might still be preferred over a lambda in certain situations:

  1. The function already exists. For example, the project might have a dependency on a utility library with a static method that does exactly what you need.
  2. The lambda would be more than a few lines long. In this case, it could be easier to read the code if it were extracted into a separate function, and passed by means of a function reference.

Objects and Object Literals

In languages where there are no first-class functions - that is, languages where you cannot pass functions into and out of other functions, or assign them to variables - the same effect is sometimes achieved by creating an object or an object literal. Here’s an example for how this looks in Kotlin. Young children and those who are sensitive to extreme verbosity should turn their eyes now…

interface Operation {
    fun execute(a: Int, b: Int): Int
}

class Example {
    fun doIt(operation: Operation) {
        println(operation.execute(1, 2))
    }

    fun add() {
        doIt(object : Operation {
            override fun execute(a: Int, b: Int): Int {
                return a + b
            }
        })
    }
}

Okay, so that wasn’t horrible, but it’s still a lot more noise than a lambda would have been.

An object could be a good choice if there’s a lot of context required in order to run the function. For example, if an entire class - with properties and other functions of its own - has to be present in order to run the function, then an object could be a reasonable approach. At that point, you basically have a Strategy Pattern anyway.

Objects or object literals might be required depending on the argument type of the receiving function.

Share this article:

  • Share on Twitter
  • Share on Facebook
  • Share on Reddit