Types of functions in Kotlin

I’ve recently begun exploring Kotlin, and it’s been a really positive experience, reminding me of my excitement in 2011 when I discovered Groovy. As a long-time fan of dynamic languages, I still find Groovy to be more advanced. However, Kotlin’s popularity makes it a compelling option for leveraging the power of the JVM. Kotlin introduces many concepts that, while familiar from other languages, have made me rethink how things are done.

This post focuses on Function Types within Kotlin code compiled for the JVM. In Kotlin, functions are first-class citizens, treated as objects. Since Kotlin is statically typed and JVM functions are always class methods, Function Types provide a mechanism for Kotlin to handle functions as objects with defined types for parameters and return values. Interestingly, this mirrors what I learned a month ago in this excellent article.

From a type-checking perspective, Kotlin’s Function Types are comparable to using Python’s Callable type with Type Hints. For instance, the Kotlin Function Type (String, Int) -> String, representing a function that takes a string and an integer as input and returns a string, is analogous to Python’s Callable[[str, int], str]. However, Function Types are more than a Kotlin facility to help with Type Checking. In the JVM everything is typed, so Function Types have to be expressed somehow at the JVM level, so in the end Function Types are syntactic sugar for an Interface containing an invoke method. As I aforementioned, when Kotlin code is compiled to Java bytecodes, Kotlin functions have to end up being methods in a class. So for each “function object” (lambda expression, anonymous function, function reference) that we define the Kotlin compiler will create a class that implements an interface that corresponds to the Function Type for that function. Decompiling with javap -c the .class files generated by kotlinc you’ll see that these are the kotlin.jvm.functions.FunctionX interfaces, for the Function Type that I’m using as example we have: kotlin.jvm.functions.Function2. This is explained here.

Kotlin also introduces Function Types with Receiver, denoted as String.(Int) -> String. These represent functions that can be called as methods on the first parameter (the receiver), like “hi”.say(2). It’s essentially syntactic sugar, as the underlying interface remains the same as the standard Function Type (String, Int) -> String, which is kotlin.jvm.functions.Function2. Consequently, Function Types with Receiver can be invoked either through the receiver or by explicitly passing it as the first parameter. Assignment between compatible Function Types, with or without a receiver, is permitted. However, invoking a regular Function Type as a compatible Function Type with Receiver is not allowed. The following code illustrates these concepts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class City constructor(var name: String, var population: Int) {
    fun multiplyPopulation(factor: Int){
        population *= factor
    }   
}

val paris = City("Paris", 2000000)

// declare a Function Type with Receiver
var multiplier2: City.(Int) -> Unit
// assign a function reference to it
multiplier2 = City::multiplyPopulation

// can invoke it both through the receiver
paris.multiplier2(2)

// or passing the receiver as parameter
multiplier2(paris, 2)

// -------------------------------

// declare a Function Type
var multiplier3: (City, Int) -> Unit = City::multiplyPopulation

multiplier3(paris, 2)

// I CAN NOT invoke a "normal function type" as a "function type with receiver"
//paris.multiplier3(2) // Compilation Error: unresolved reference: multiplier3

// I can assign a "normal" Function Type to a compatible Function Type with Receiver
multiplier2 = multiplier3
// and invoke it one way or another
multiplier2(paris, 2)
paris.multiplier2(2)

// I can also assign a Function Type with Receiver to a "normal" Function Type
multiplier3 = multiplier2
multiplier3(paris, 2)

// but as expected this does not compile
// paris.multiplier3(2)

While the previous examples might not showcase the full potential of Function Types with Receiver, their true power lies in using them with Lambda Expressions with Receiver as function parameters. A prime example is apply scope function. This allows for code like:

1
2
3
4
city.apply {
	name = "a"
	age = 47
}

This works because the apply function is designed defined to receive a Function Type with Receiver, as shown below:

1
2
3
4
public inline fun T.apply(block: T.() -> Unit): T {
    block()
    return this
}

Kotlin developers, particularly those from a Java background, might appreciate the “implicit this” feature (block() is equivalent to this.block(), and name = “a” is the same as this.name = “a”). However, coming from JavaScript and Python, where “this” and “self” require explicit handling, I find this implicit approach can sometimes make the code less clear.

Licensed under CC BY-NC-SA 4.0
Last updated on Dec 08, 2023 20:24 +0100