article banner

Context receivers

This is a chapter from the book Functional Kotlin. You can find it on LeanPub or Amazon. It is also available as a course.

Context parameters were added in Kotlin 1.6.20 and do not work in earlier versions. What is more, to enable this experimental feature in that version, one needs to add the "-Xcontext-receivers" compiler argument.

There are two kinds of problems that extension functions help us solve. The first one is quite intuitive: extending types with additional methods. This is basically what extension functions are designed for. So, for instance, if you need the capitalize method on String or the product method on Iterable<Int>, nothing is lost as you can always add these methods using an extension function.

fun String.capitalize() = this .replaceFirstChar(Char::uppercase) fun Iterable<Int>.product() = this .fold(1, Int::times) fun main() { println("alex".capitalize()) // Alex println("this is text".capitalize()) // This is text println((1..5).product()) // 120 println(listOf(1, 3, 5).product()) // 15 }

The second kind of use case is less obvious but also quite common. We turn functions into extensions to explicitly pass a context of their use. Let's take a look at a few examples.

Consider a situation in which you use Kotlin HTML DSL, and you want to extract some structures into a function. We might use the DSL we defined in the Type Safe DSL Builders chapter and define a standardHead function that sets up a standard head. Such a function needs a reference to HtmlBuilder, which we might provide as an extension receiver.

fun HtmlBuilder.standardHead() { head { title = "My website" css("Some CSS1") css("Some CSS2") } } val html = html { standardHead() body { h1("Title") h3("Subtitle 1") +"Some text 1" h3("Subtitle 2") +"Some text 2" } }

Defining an extension function in a case like this is very popular because it is very convenient. However, this is not what extension functions were initially designed for: we do not intend to call standardHead on an object of type HtmlBuilder. Instead, we want it to be used where there is a receiver of type HtmlBuilder. An extension in such a use case is used to receive a context. We should prefer a dedicated feature for just receiving a context. Why? Let's consider the essential extension function problems with this use case.

Extension function problems

Extension functions were designed to define new methods to call on objects, so they do not work well when used to receive a context. Here are the most important problems:

  • extension functions are limited to a single receiver,
  • using an extension receiver to pass a context gives a false impression of this function’s meaning and how it should be called,
  • an extension function can only be called on a receiver object.

Let's discuss these problems in detail.

Extension functions are limited to a single receiver. This makes a lot of sense when we define extension functions as methods to call on objects, but not when we want to use them to pass a receiver.

For example, when we use Kotlin Coroutines, we often want to launch a flow on a coroutine scope[12_1]. A scope is often used as a receiver, but the function used to launch it is already an extension on Flow<T>, so it cannot also be an extension on CoroutineScope. As a result we have the launchIn function, which expects CoroutineScope as a regular argument and is often called as launchIn(this).

import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.* fun <T> Flow<T>.launchIn(scope: CoroutineScope): Job = scope.launch { collect() } suspend fun main(): Unit = coroutineScope { flowOf(1, 2, 3) .onEach { print(it) } .launchIn(this) }

Using an extension receiver to pass a context gives a false impression of this function’s meaning and how it should be called. To understand this, consider the sendNotification function, which sends a notification to a user. Its additional functionality is displaying info using a logger. Let's say that in our application we make our classes implement LoggerContext to be able to use a logger implicitly. When we call sendNotification, we need to pass this LoggingContext somehow, and the most convenient way is as a receiver. So, we define sendNotification as an extension function on LoggerContext. However, this is a very poor design choice because it suggests that sendNotification is a method on LoggingContext, which is not true.

interface LoggingContext { val logger: Logger } fun LoggingContext.sendNotification( notification: NotificationData ) { logger.info("Sending notification $notification") notificationSender.send(notification) }

An extension function can only be called on objects, which is precisely why extension functions were invented, but this is not great when we want to use extension functions to pass a receiver implicitly. Consider standardHead from the example above. We want to use it as a part of HTML DSL, but we do not want to allow it to be called on an object of type HtmlBuilder

// This is how we want standardHead used html { standardHead() } // or this with(receiver) { standardHead() } // but not this builder.standardHead()

To address all these problems, Kotlin introduced a feature called context parameters.

Introducing context parameters

Kotlin 1.6.20 introduced a new feature that is dedicated to passing implicit receivers into functions. This feature is called context parameters and it addresses all the aforementioned issues. How do we use it? For any function, we can specify the context parameter types inside brackets after the context keyword. Such functions have receivers of specified types, and these functions need to be called in the scope where all the specified receivers are.

class Foo { fun foo() { print("Foo") } } context(Foo) fun callFoo() { foo() } fun main() { with(Foo()) { callFoo() } }

Importantly, a context parameter function call expects an implicit receiver, so such functions cannot be called on an object of receiver type.

fun main() { Foo().callFoo() // ERROR }

When you want to use an explicit context parameter, you need to specify a label after this with a type that specifies which receiver you want to use.

context(Foo)
fun callFoo() {
   this@Foo.foo() // OK
   this.foo() // ERROR, this is not defined
}

Context parameters can specify multiple receiver types. For example, in the code below, the callFooBoo function expects both Foo and Boo receiver types.

class Foo { fun foo() { print("Foo") } } class Boo { fun boo() { println("Boo") } } context(Foo, Boo) fun callFooBoo() { foo() boo() } context(Foo, Boo) fun callFooBoo2() { callFooBoo() } fun main() { with(Foo()) { with(Boo()) { callFooBoo() // FooBoo callFooBoo2() // FooBoo } } with(Boo()) { with(Foo()) { callFooBoo() // FooBoo callFooBoo2() // FooBoo } } }

A receiver is anything that this represents. It might be an extension function receiver, a lambda expression receiver, or a dispatch receiver (the enclosing class for methods and properties). One receiver can be used for multiple expected types. For example, in the code below, inside the method call in FooBoo, we use a dispatch receiver for both Foo and Boo types.

package fgfds interface Foo { fun foo() { print("Foo") } } interface Boo { fun boo() { println("Boo") } } context(Foo, Boo) fun callFooBoo() { foo() boo() } class FooBoo : Foo, Boo { fun call() { callFooBoo() } } fun main() { val fooBoo = FooBoo() fooBoo.call() // FooBoo }

Use cases

Now, let's see how context parameters address the aforementioned issues. We could use a context parameter to define that standardHead needs to be called on HtmlBuilder. This way should be preferred over using an extension function.

context(HtmlBuilder) fun standardHead() { head { title = "My website" css("Some CSS1") css("Some CSS2") } }

Context parameters are a better choice for most functions that should be used on a DSL and DSL definitions.

The function that is used to launch a flow can also benefit from context parameters functionality. We could define a launchFlow extension function on Flow<T> with the CoroutineScope context parameter. Such a function needs to be called on a flow in a scope where CoroutineScope is a receiver.

import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.* context(CoroutineScope) fun <T> Flow<T>.launchFlow(): Job = this@CoroutineScope.launch { collect() } suspend fun main(): Unit = coroutineScope { flowOf(1, 2, 3) .onEach { print(it) } .launchFlow() }

Now, consider the sendNotification function, which needed LoggingContext, but we did not want it to be defined as an extension function. We could provide LoggingContext using a context parameter.

context(LoggingContext) fun sendNotification(notification: NotificationData) { logger.info("Sending notification $notification") notificationSender.send(notification) }

Now let's see some other examples. Consider an external DSL builder where you can add items using the addItem method.

fun myChristmasLetter() = christmasLetter { title = "My presents list" addItem("Cookie") addItem("Drawing kit") addItem("Poi set") }

Let's say that you want to extend this builder to make it possible to define items using the unary plus operator on String instead:

fun myChristmasLetter() = christmasLetter { title = "My presents list" +"Cookie" +"Drawing kit" +"Poi set" }

To do that, we need to define a unaryPlus operator function which is an extension on String. However, we also need a receiver that will let us add elements using the addItem function. To do that, we can use a context parameter.

context(ChristmasLetterBuilder) operator fun String.unaryPlus() { addItem(this) }

A popular Android example of using a context receiver is defining dp size (density-independent pixels) in code. This is a standard way of describing width or height. The problem is that dp size depends on a view because it depends on display density. The solution is that the dp extension property might have a context parameter of View type. Then, such a property can quickly and conveniently be used as a part of view builders.

context(View) val Float.dp get() = this * resources.displayMetrics.density context(View) val Int.dp get() = this.toFloat().dp

As you can see, there are many cases in which context receiver functionality is useful, but remember that most of them are related to DSL builders.

Concerns

Like every good feature, context parameters can also be used poorly, which could lead to code that is more complicated or less safe than it needs to be. We should use this feature only where it makes sense, and using too many receivers in our code is not good for readability. Implicit function calls are not as clear as explicit ones. There are also risks of name collisions. Receivers are not as visible as arguments. Using implicit receivers too often can make code confusing for other developers. I suggest not using context receivers if they often need wrapping function calls using scope functions, like with.

// Don't do this context( LoggerContext, NotificationSenderProvider, // not a context NotificatonsRepository // not a context ) // it might hard to call such a function suspend fun sendNotifications() { log("Sending notifications") val notifications = getUnsentNotifications() // unclear val sender = create() // unclear for (n in notifications) { sender.send(n) } log("Notifications sent") } class NotificationsController( notificationSenderProvider: NotificationSenderProvider, notificationsRepository: NotificationsRepository ) : Logger() { @Post("send") suspend fun send() { with(notificationSenderProvider) { // avoid such calls with(notificationsRepository) { //avoid such calls sendNotifications() } } } }

In general, my suggestions are:

  • When there is no good reason to use a context parameter, prefer using a regular argument.
  • When it is unclear from which receiver a method comes, consider using an argument instead of the receiver or use this receiver explicitly[12_2].
// Don't do that context(LoggerContext) suspend fun sendNotifications( notificationSenderProvider: NotificationSenderProvider, notificationsRepository: NotificationsRepository ) { log("Sending notifications") val notifications = notificationsRepository .getUnsentNotifications() val sender = notificationSenderProvider.create() for (n in notifications) { sender.send(n) } log("Notifications sent") } class NotificationsController( notificationSenderProvider: NotificationSenderProvider, notificationsRepository: NotificationsRepository ) : Logger() { @Post("send") suspend fun send() { sendNotifications( notificationSenderProvider, notificationsRepository ) } }

Named context parameters

There are plans for context parameters to allow naming received values, so we can use them explicitly by their names.

context(view: View) val Float.dp get() = this * view.resources.displayMetrics.density

It will also be possible to ignore the receiver name by using an underscore.

context(_: AnalysisScope) val Type.isBoolean: Boolean = this.equalTo(BuiltIns.Boolean)

Summary

Kotlin introduced a new prototype feature called context parameters to address situations in which we want to pass receivers into functions or classes implicitly. Until now, we used extension functions for this, but their biggest issues were:

  • extension functions are limited to a single receiver,
  • using an extension receiver to pass a context gives a false impression of this function’s meaning and how it should be called,
  • an extension function can only be called on a receiver object.

Context parameters solve all these problems and are very convenient. I’m looking forward to them becoming a stable feature that I can use in my projects.

[12_1]: More about this in the Kotlin Coroutines: Deep Dive book. [12_2]: See Effective Kotlin, Item 14§: Consider referencing receivers explicitly.