I've recently started to fiddle with Kotlin and it's being a really nice experience. It reminds me a bit of my excitement in 2011 when I discovered Groovy. Well, as I've always been a fan of dynamic languages I would say that Groovy still seems more advanced to me, but given how much attention Kotlin has gained, Kotlin seems to me the best option to leverage the power of a JVM. There are tons of concepts that suddenly come up when you start to learn Kotlin, most of them sound more or less familiar from other languages, but they've helping me to rethink how I do things in other languages, why other languages do things differently, etc.
I'll talk today about Function Types, and anything that I'll say in this post is related to Kotlin code compiled for the JVM. So in Kotlin, as in any other modern language, functions are first-class citizens. They are like objects that can be assigned, passed around and returned. Kotlin is statically typed, and for the JVM functions are not objects, they are always methods of a class. Function Types are the mechanism that allows Kotlin to manage function as objects with well defined types (the type of their parameters and of its return). Hum, I've just realized that I have just rewritten with other words what I learnt 1 month ago in this excellent article.
From a Type Checking point of view Function Types are more or less the same that when in Python we use the Callable type with Type Hints. A Kotlin Function Type like this: (String, Int) -> String that represents a function that receives a string and an int and returns a string is equivalent to this Type Hint in Python: 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<java.lang.String, java.lang.Integer, java.lang.String>. This is explained here.
Kotlin has also Function Types with Receiver, something like: String.(Int) -> String. This is used to represent functions that can be invoked as if they were methods of the first parameter (the receiver), I mean, "hi".say(2). We could say that they are an additional layer of Syntax Sugar, cause the interface for String.(Int) -> String is the same interface that we saw before for (String, Int) -> String, that is: kotlin.jvm.functions.Function2<java.lang.String, java.lang.Integer, java.lang.String>. Given that in the end both Function Types represent the same interface a Function Type with Receiver can be invoked through the receiver or with the receiver as its first explicit parameter. Also, I can do assignmet between compatible types of Funtion Types with and without receiver. What is not allowed is invoking a normal Function Type as a compatible Function Type with Receiver. I hope the code below makes a bit more clear what I mean.
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)
With the examples that I've used so far one could think that this idea of Function Types with Receiver is not particularly interesting. Being able to write "a".say(2) rather than say("a", 2) is not something that makes the code particularly more expressive or beautiful. Right, but I think the main idea of Functions Types with receiver is using them with Lambda Expressions with Receiver when passing them as parameters to another function. The apply scope function is the perfect example. We can write code like this:
city.apply {
name = "a"
age = 47
}
because the apply function is defined as receiving a Function Type with Receiver, like this:
public inline fun T.apply(block: T.() -> Unit): T {
block()
return this
}
I guess Kotlin guys (some of them coming from Java) will love the "implicit this" feature (the block() line is the same as this.block(), the name = "a" is the same as this.name = "a"), but for me sometimes it makes code less straightforward. I guess it's because I've got so used to JavaScript, where "this" can be dynamic or "static" ("bound" through function.bind or trapped as "lexical this" in arrow functions), but you have to explicitly type it, and to Python where the first parameter of a method is just named "self" by convention and you have to type it both in the method signature and when using it.
No comments:
Post a Comment