
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 Kotlin1.6.20and 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.
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 }
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" } }
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 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.
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) }
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) }
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()
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() } }
fun main() { Foo().callFoo() // ERROR }
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
}
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 } } }
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 }
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") } }
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() }
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) }
addItem method.fun myChristmasLetter() = christmasLetter { title = "My presents list" addItem("Cookie") addItem("Drawing kit") addItem("Poi set") }
String instead:fun myChristmasLetter() = christmasLetter { title = "My presents list" +"Cookie" +"Drawing kit" +"Poi set" }
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) }
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
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() } } } }
- 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 ) } }
context(view: View) val Float.dp get() = this * view.resources.displayMetrics.density
context(_: AnalysisScope) val Type.isBoolean: Boolean = this.equalTo(BuiltIns.Boolean)
- 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.
[12_2]: See Effective Kotlin, Item 14§: Consider referencing receivers explicitly.
