Item 15: Consider referencing receivers explicitly

One common place where we might choose a longer structure to make something explicit is when we want to highlight that a function or a property is taken from the receiver instead of being a local or top-level variable. In the most basic situation it means a reference to the class to which the method is associated:

  1. class User: Person() {
  2. private var beersDrunk: Int = 0
  3. fun drinkBeers(num: Int) {
  4. // ...
  5. this.beersDrunk += num
  6. // ...
  7. }
  8. }

Similarly, we may explicitly reference an extension receiver (this in an extension method) to make its use more explicit. Just compare the following Quicksort implementation written without explicit receivers:

  1. fun <T : Comparable<T>> List<T>.quickSort(): List<T> {
  2. if (size < 2) return this
  3. val pivot = first()
  4. val (smaller, bigger) = drop(1)
  5. .partition { it < pivot }
  6. return smaller.quickSort() + pivot + bigger.quickSort()
  7. }

With this one written using them:

  1. fun <T : Comparable<T>> List<T>.quickSort(): List<T> {
  2. if (this.size < 2) return this
  3. val pivot = this.first()
  4. val (smaller, bigger) = this.drop(1)
  5. .partition { it < pivot }
  6. return smaller.quickSort() + pivot + bigger.quickSort()
  7. }

The usage is the same for both functions:

  1. listOf(3, 2, 5, 1, 6).quickSort() // [1, 2, 3, 5, 6]
  2. listOf("C", "D", "A", "B").quickSort() // [A, B, C, D]

Many receivers

Using explicit receivers can be especially helpful when we are in the scope of more than one receiver. We are often in such a situation when we use the apply, with or run functions. Such situations are dangerous and we should avoid them. It is safer to use an object using explicit receiver. To understand this problem, see the following code2:

  1. class Node(val name: String) {
  2. fun makeChild(childName: String) =
  3. create("$name.$childName")
  4. .apply { print("Created ${name}") }
  5. fun create(name: String): Node? = Node(name)
  6. }
  7. fun main() {
  8. val node = Node("parent")
  9. node.makeChild("child")
  10. }

What is the result? Stop now and spend some time trying to answer yourself before reading the answer.

It is probably expected that the result should be “Created parent.child”, but the actual result is “Created parent”. Why? To investigate, we can use explicit receiver before name:

  1. class Node(val name: String) {
  2. fun makeChild(childName: String) =
  3. create("$name.$childName")
  4. .apply { print("Created ${this.name}") }
  5. // Compilation error
  6. fun create(name: String): Node? = Node(name)
  7. }

The problem is that the type this inside apply is Node? and so methods cannot be used directly. We would need to unpack it first, for instance by using a safe call. If we do so, result will be finally correct:

  1. class Node(val name: String) {
  2. fun makeChild(childName: String) =
  3. create("$name.$childName")
  4. .apply { print("Created ${this?.name}") }
  5. fun create(name: String): Node? = Node(name)
  6. }
  7. fun main() {
  8. val node = Node("parent")
  9. node.makeChild("child")
  10. // Prints: Created parent.child
  11. }

This is an example of bad usage of apply. We wouldn’t have such a problem if we used also instead, and call name on the argument. Using also forces us to reference the function’s receiver explicitly the same way as an explicit receiver. In general also and let are much better choice for additional operations or when we operate on a nullable value.

  1. class Node(val name: String) {
  2. fun makeChild(childName: String) =
  3. create("$name.$childName")
  4. .also { print("Created ${it?.name}") }
  5. fun create(name: String): Node? = Node(name)
  6. }

When receiver is not clear, we either prefer to avoid it or we clarify it using explicit receiver. When we use receiver without label, we mean the closest one. When we want to use outer receiver we need to use a label. In such case it is especially useful to use it explicitly. Here is an example showing them both in use:

  1. class Node(val name: String) {
  2. fun makeChild(childName: String) =
  3. create("$name.$childName").apply {
  4. print("Created ${this?.name} in "+
  5. " ${this@Node.name}")
  6. }
  7. fun create(name: String): Node? = Node(name)
  8. }
  9. fun main() {
  10. val node = Node("parent")
  11. node.makeChild("child")
  12. // Created parent.child in parent
  13. }

This way direct receiver clarifies what receiver do we mean. This might be an important information that might not only protect us from errors but also improve readability.

DSL marker

There is a context in which we often operate on very nested scopes with different receivers, and we don’t need to use explicit receivers at all. I am talking about Kotlin DSLs. We don’t need to use receivers explicitly because DSLs are designed in such a way. However, in DSLs, it is especially dangerous to accidentally use functions from an outer scope. Think of a simple HTML DSL we use to make an HTML table:

  1. table {
  2. tr {
  3. td { +"Column 1" }
  4. td { +"Column 2" }
  5. }
  6. tr {
  7. td { +"Value 1" }
  8. td { +"Value 2" }
  9. }
  10. }

Notice that by default in every scope we can also use methods from receivers from the outer scope. We might use this fact to mess with DSL:

  1. table {
  2. tr {
  3. td { +"Column 1" }
  4. td { +"Column 2" }
  5. tr {
  6. td { +"Value 1" }
  7. td { +"Value 2" }
  8. }
  9. }
  10. }

To restrict such usage, we have a special meta-annotation (an annotation for annotations) that restricts implicit usage of outer receivers. This is the DslMarker. When we use it on an annotation, and later use this annotation on a class that serves as a builder, inside this builder implicit receiver use won’t be possible. Here is an example of how DslMarker might be used:

  1. @DslMarker
  2. annotation class HtmlDsl
  3. fun table(f: TableDsl.() -> Unit) { /*...*/ }
  4. @HtmlDsl
  5. class TableDsl { /*...*/ }

With that, it is prohibited to use outer receiver implicitly:

  1. table {
  2. tr {
  3. td { +"Column 1" }
  4. td { +"Column 2" }
  5. tr { // COMPILATION ERROR
  6. td { +"Value 1" }
  7. td { +"Value 2" }
  8. }
  9. }
  10. }

Using functions from an outer receiver requires explicit receiver usage:

  1. table {
  2. tr {
  3. td { +"Column 1" }
  4. td { +"Column 2" }
  5. this@table.tr {
  6. td { +"Value 1" }
  7. td { +"Value 2" }
  8. }
  9. }
  10. }

The DSL marker is a very important mechanism that we use to force usage of either the closest receiver or explicit receiver usage. However, it is generally better to not use explicit receivers in DSLs anyway. Respect DSL design and use it accordingly.

Summary

Do not change scope receiver just because you can. It might be confusing to have too many receivers all giving us methods we can use. Explicit argument or reference is generally better. When we do change receiver, using explicit receivers improves readability because it clarifies where does the function come from. We can even use a label when there are many receivers to clarify from which one the function comes from. If you want to force using explicit receivers from the outer scope, you can use DslMarker meta-annotation.