Every so often, you might come across the strange sight of ::
in your Kotlin code.
This cheeky little four-eyed fella is the Callable Reference Operator. Implementing it creates a callable reference from a function, property or class.
What is a callable reference?
A callable reference is a separate object that represents a function, property or class. It does not execute immediately, rather it holds the reference until the moment that it is invoked.
For example:
fun add(a: Int, b: Int): Int {
return a + b
}
val calculateNow = add(3, 5)
// ππ» this variable makes a direct call to the 'add' function.
// It will execute immediately
val calculateLater = ::add
// ππ» this variable makes a callable reference of the 'add' function.
// It will only execute when the variable is invoked (as below ππ»)
calculateLater(2, 5)
The Kotlin.reflect API
The reference we create by using the Callable Reference Operator is always an object from the kotlin.reflect API (eg. an instance of KFunction, KProperty, or KClass). We can see this when we hover over our callable reference in the IDE:
KFunction2
tells us we are representing a function that has two parameters.
<Int, Int, Int>
tells us the parameters are of type Int, as is the return type.
When might we use the Callable Reference Operator?
A callable reference is 'first-class' which means that it can be passed as an argument, stored in a variable, or returned from a function - just as any other data type - a Boolean, String, Int, etc.. This makes it particularly useful for passing into higher order functions.
The Callable Reference Operator in action
In the below examples, I demonstrate how the Callable Reference Operator can be used as a concise alternative to passing lambdas to functions in a few different case scenarios.
A function reference (::functionName)
fun isOdd(x: Int) = x % 2 != 0
val numbers = listOf(1, 2, 3)
numbers.filter(::isOdd) // [1,3]
// the above line does the same as ππ»
numbers.filter { isOdd(it) } // [1,3]
A property reference (ClassName::propertyName)
val strings = listOf("a", "bc", "def")
strings.map(String::length) // [1, 2, 3]
// the above line does the same as ππ»
strings.map { it.length } // [1, 2, 3]
Constructor reference (::ClassName)
data class Person(val name: String, val age: Int)
val personGenerator: (String, Int) -> Person = ::Person
// the above line does the same as ππ»
val personGeneratorUsingLambda: (String, Int) -> Person = { nameParam, ageParam ->
Person(nameParam, ageParam)
}
// we can also treat ::Person as a first class value in the following higher-order function
fun createPeopleFromData(data: List<Pair<String, Int>>, personGenerator: (String, Int) -> Person): List<Person> {
return data.map { (name, age) -> personGenerator(name, age)}
}
val listOfPeople = listOf(
"Sidney" to 2,
"Alf" to 5,
"Morag" to 10
)
val people = createPeopleFromData(listOfPeople, ::Person)
// the above line does the same as ππ»
val peopleUsingLambda = createPeopleFromData(listOfPeopleData) { nameParam, ageParam ->
Person(nameParam, ageParam)
}
In this last example, you can really see how the code becomes more concise when we use the Callable Reference Operator.
Conclusion
Using the Callable Reference Operator is pretty neat. However, a clever, concise way of programming is only useful if it's also easy to understand for any other engineer who might come across it.
I don't come across the Callable Reference Operator very much in production code which implies one of two things: either there is a lack of knowledge / confidence in using it or that the knowledge is there but that engineers find it less intuitive or think it makes code less readable.
Personally, I don't currently read the Callable Reference Operator as fluently as I might read a lambda. But this will no doubt start to even out as I gain experience around using (and reading) this operator. I will certainly be looking out for it more in code and also look forward to utilising it more in my own work.
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.