13 minutes read

As you know, lambda expressions allow you to use code as data and pass it as a function's arguments. Another way to do it is to use function references. They are often even more readable than corresponding lambda expressions. Besides, function references force developers to decompose a program into a set of short functions with clear areas of responsibility.

Make code clearer with function references

A quick reminder: a function reference is a special link that refers to a certain function by its name and can be called at any time whenever we need it. Let’s look at an example of how we can do that:

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

fun isEven(x: Int) = x % 2 == 0

fun printNumbers(numbers: MutableList<Int>, filter: (Int) -> Boolean) {
    for (number in numbers) {
        if (filter(number))
            print("$number ")
    }
}

fun main() {
    val numbers = mutableListOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
    val oddFunction = ::isOdd
    print("Odd numbers: ")
    printNumbers(numbers, oddFunction)
    print("\nEven numbers: ")
    printNumbers(numbers, ::isEven)
}

As you can see, we can call functions isOdd() and isEven() either directly or by passing their value using ::.

The result of the program will be:

Odd numbers: 1 3 5 7 9 
Even numbers: 2 4 6 8 10 

It's a common approach to pass a function to another method. Of course you can use lambdas for this, but if you already have a function why not use a link and avoid code duplication. In this topic, you will learn how to create a reference to various functions.

Reference by class

Also, you can reference functions that belong to a class. The base syntax in such cases looks like this:

objectOrClass::functionName

Here, objectOrClass can be a class name or a particular instance of a class.

Let's take a look at an example with the class Person:

class Person(val name: String, val lastname: String) {

    fun printFullName(): String {
        return("full name: $name $lastname")
    }
}

Here, we create a function reference:

val person: Person = Person("Sara", "Rogers")
val personFun: () -> String = person::printFullName

And now, we call the function by using a special function invoke:

print(personFun.invoke())

Actually, you can also call the function without the function invoke:

print(personFun())

And here is the result:

full name: Sara Rogers

Now you see how you can easily turn a function into an object by using a reference to it.
This is a very useful skill in programming, and when you start working on serious projects, it will help you with many problems.

Let's see some other possibilities of function references.

Standard classes and function reference

Function references also work with functions from Kotlin standard classes. Let's see an example.

We create a reference to the standard function dec of the Int class. The function dec decreases the number by one (decrement).

val dec: (Int) -> Int = Int::dec

Here, Int::dec is a reference to a function.

This code works because the definition of the function operator fun dec(): Int fits the type (Int) -> Int: they both mean taking one integer argument and returning an integer value.

Now we have the dec object that can be used as a function. Let's invoke it!

print(dec(4)) // 3

So, once assigned to an object, a function reference works in the same way as a lambda expression.

Here is an alternative way to create the same object using a lambda expression:

val dec: (Int) -> Int = {x -> x.dec()}

It is recommended to use function references rather than lambda expressions if you just need to invoke a standard function without other operations. Your code will be shorter, more readable, and easier to test.

Note that we can refer to both standard and our custom functions using function references.

Kinds of function references

In general, there are four kinds of function references:

  1. reference to a function;

  2. reference by Class;

  3. reference by Object;

  4. reference to a constructor.

1. Reference to a function

This reference has the following declaration:

::functionName

Let's see an example of using a reference to functions multiply and add:

fun multiply(x: Int, y: Int) = x * y

fun add(x: Int, y: Int) = x + y

fun main() {
    val operatorMultiply: (Int, Int) -> Int = ::multiply
    val operatorAdd: (Int, Int) -> Int = ::add
}

Now we can invoke both functions operatorMultiply and operatorAdd for a pair of values and see the result:

operatorMultiply(10, 5) // 50
operatorAdd(5, 4) // 9

The operatorMultiply and operatorAdd functions can be also written using the following lambda expression:

val operatorMultiply: (Int, Int) -> Int = {x: Int, y: Int -> x * y}
val operatorAdd: (Int, Int) -> Int = {x: Int, y: Int -> x + y}

2. Reference by Class

The general form is the following:

ClassName::functionName

Let's take a look at the function and of the class Int, which allow us to do logical AND with two binary numbers:

Here's an example of how this function works:

val a = 5 and 4 // 101 & 100 = 100 (4)

Also, we can call the function and in another way:

val b = 9.and(3) // 1001 & 0011 = 0001 (1)

So, and is the function of class Int and we can get the reference to it:

val and: (Int, Int) -> Int = Int::and

Now we can invoke the and function with two values, for example, 1 and 3:

and(1, 3) // the result is 1

The and function can be also written using the following lambda expression:

val and: (Int, Int) -> Int = {a: Int, b: Int -> a.and(b)}

3. Reference by Object

The general form looks like this:

objectName::functionName

Let's check out an example of a reference to the indexOf function from a particular string; the function finds the index of the first occurrence of an element in the text. This function takes a string to find, an index from which we start the search, and a Boolean that determines whether the case will be ignored when matching a character (by default it is false).

val whatsGoingOnText: String = "What's going on here?"
val indexWithinWhatsGoingOnText: (String, Int, Boolean) -> Int = whatsGoingOnText::indexOf

Here is the result of applying it to different arguments:

println(indexWithinWhatsGoingOnText("going", 0, true)) // 7
println(indexWithinWhatsGoingOnText("Hi", 0, true))  // -1
println(indexWithinWhatsGoingOnText("what's", 0, false))  // -1
println(indexWithinWhatsGoingOnText("what's", 0, true))  // 0

As you can see, we actually always work with the whatsGoingOnText object captured from the context.

The following example of a lambda expression is a full equivalent of the above reference and can help you better understand the situation:

val indexWithinWhatsGoingOnText: (String, Int, Boolean) -> Int =
        { string: String, startIndex: Int, ignoreCase: Boolean ->
            whatsGoingOnText.indexOf(string, startIndex, ignoreCase)
        }

4. Reference to a constructor

This reference has the following declaration:

::ClassName

For example, let's consider our custom class Person with a single field name.

class Person (val name: String){
}

Here is a reference to the constructor of this class:

val personGenerator: (String) -> Person = ::Person

This function produces new Person objects based on their names.

val johnFoster: Person = personGenerator("John Foster")

Here is the corresponding lambda expression that does the same:

val personGenerator: (String) -> Person = {string -> Person(string)}

Further, we will use lambda expressions and function references together.

Let's sum up the new knowledge:

Type

Function reference

Reference to a function

::functionName

Reference by Class

Class::functionName

Reference by Object

object::functionName

Reference to a constructor

::Class

More practical examples will be explored in the following topics. For now, it is enough to grasp the general idea and the syntax of function references.

Conclusion

You've learned a new way to create function objects by using function references. It has a lot in common with lambda expressions but allows writing more readable and decomposed code. Having covered this topic, you should memorize all four types of function references and be ready to use them in your programs when you need to pass a piece of code to some function.

170 learners liked this piece of theory. 13 didn't like it. What about you?
Report a typo