What is a “receiver” in Kotlin?

2019-01-10 09:41发布

问题:

How is it related to extension functions? Why is with a function, not a keyword?

There appears to be no explicit documentation for this topic, only the assumption of knowledge in reference to extensions.

回答1:

It is true that there appears to be little existing documentation for the concept of receivers (only a small side note related to extension functions), which is surprising given:

  • their existence springing out of extension functions;
  • their role in building a DSL using said extension functions;
  • the existance of a standard library function with, which given no knowledge of receivers might look like a keyword;
  • a completely seperate syntax for function types.

All these topics have documentation, but nothing goes in-depth on receivers.


First:

What's a receiver?

Any block of code in Kotlin may have a (or even multiple) types as a receiver, making functions and properties of the receiver available in that block of code without qualifiying it.

Imagine a block of code like this:

{ toLong() }

Doesn't make much sense, right? In fact, assigning this to a function type of (Int) -> Long - where Int is the (only) parameter, and the return type is Long - would rightfully result in a compilation error. You can fix this by simply qualifying the function call with the implicit single parameter it. However, for DSL building, this will cause a bunch of issues:

  • Nested blocks of DSL will have their upper layers shadowed:
    html { it.body { // how to access extensions of html here? } ... }
    This may not cause issues for a HTML DSL, but may for other use cases.
  • It can litter the code with it calls, especially for lambdas that use their parameter (soon to be receiver) a lot.

This is where receivers come into play.

By assigning this block of code to a function type that has Int as a receiver (not as a parameter!), the code suddenly compiles:

val intToLong: Int.() -> Long = { toLong() }

Whats going on here?


A little sidenote

This topic assumes familarity with function types, but a little side note for receivers is needed.

Function types can also have one receiver, by prefixing it with the type and a dot. Examples:

Int.() -> Long  // taking an integer as receiver producing a long
String.(Long) -> String // taking a string as receiver and long as parameter producing a string
GUI.() -> Unit // taking an GUI and producing nothing

Such function types have their parameter list prefixed with the receiver type.


Resolving code with receivers

It is actually incredibly easy to understand how blocks of code with receivers are handled:

Imagine that, similiar to extension functions, the block of code is evaluated inside the class of the receiver type. this effectively becomes amended by the receiver type.

For our earlier example, val intToLong: Int.() -> Long = { toLong() } , it effectively results in the block of code being evaluated in a different context, as if it was placed in a function inside Int. Here's a different example using handcrafted types that showcases this better:

class Bar

class Foo {
    fun transformToBar(): Bar = TODO()
}

val myBlockOfCodeWithReceiverFoo: (Foo).() -> Bar = { transformToBar() }

effectively becomes (in the mind, not code wise - you cannot actually extend classes on the JVM):

class Bar 

class Foo {
    fun transformToBar(): Bar = TODO()

    fun myBlockOfCode(): Bar { return transformToBar() }
}

val myBlockOfCodeWithReceiverFoo: (Foo) -> Bar = { it.myBlockOfCode() }

Notice how inside of a class, we don't need to use this to access transformToBar - the same thing happens in a block with a receiver.

It just so happens that the documentation on this also explains how to use an outermost receiver if the current block of code has two receivers, via a qualified this.


Wait, multiple receivers?

Yes. A block of code can have multiple receivers, but this currently has no expression in the type system. The only way to archieve this is via multiple higher-order functions that take a single receiver function type. Example:

class Foo
class Bar

fun Foo.functionInFoo(): Unit = TODO()
fun Bar.functionInBar(): Unit = TODO()

inline fun higherOrderFunctionTakingFoo(body: (Foo).() -> Unit) = body(Foo())
inline fun higherOrderFunctionTakingBar(body: (Bar).() -> Unit) = body(Bar())

fun example() {
    higherOrderFunctionTakingFoo {
        higherOrderFunctionTakingBar {
            functionInFoo()
            functionInBar()
        }
    }
}

Do note that if this feature of the Kotlin language seems inappropriate for your DSL, @DslMarker is your friend!


Conclusion

Why does all of this matter? With this knowledge:

  • you now understand why you can write toLong() in an extension function on a number, instead of having to reference the number somehow. Maybe your extension function shouldn't be an extension?
  • You can build a DSL for your favorite markup language, maybe help parsing the one or other (who needs regular expressions?!).
  • You understand why with, a standard library function and not a keyword, exists - the act of amending the scope of a block of code to save on redudant typing is so common, the language designers put it right in the standard library.
  • (maybe) you learned a bit about function types on the offshoot.


回答2:

Function Literals/Lambda with Receiver

Kotlin supports the concept of “function literals with receivers”. It enables the access on visible methods and properties of a receiver of a lambda in its body without any additional qualifiers. This is very similar to extension functions in which it’s also possible to access visible members of the receiver object inside the extension.

A simple example, also one of the greatest functions in the Kotlin standard library, isapply:

public inline fun <T> T.apply(block: T.() -> Unit): T { block(); return this }

As you can see, such a function literal with receiver is taken as the argument block here. This block is simply executed and the receiver (which is an instance of T) is returned. In action this looks as follows:

val foo: Bar = Bar().apply {
    color = RED
    text = "Foo"
}

We instantiate an object of Bar and call apply on it. The instance of Bar becomes the “receiver”. The block, passed as an argument in {}(lambda expression) does not need to use additional qualifiers to access and modify the shown visible properties color and text.

The concept of lambdas with receiver is also the most important feature for writing DSLs with Kotlin.



回答3:

var greet: String.() -> Unit = { println("Hello $this") }

this defines a variable of type String.() -> Unit, which tells you

  • String is the receiver
  • () -> Unit is the function type

Like F. George mentioned above, all methods of this receiver can be called in the method body.

So, in our example, this is used to print the String. The function can be invoked by writing...

greet("Fitzgerald") // result is "Hello Fitzgerald"

the above code snippet was taken from Kotlin Function Literals with Receiver – Quick Introduction by Simon Wirtz.



标签: kotlin