21-Dec

Kotlin

Receivers

3 min read

·

By Vegard Veiset

·

December 21, 2019

Did you know that you can change the scope of lambdas? It’s pretty awesome. In Kotlin you can change the scope of lambdas by using a concept called receivers, and the Kotlin standard library does it all the time.

Let's take a closer look at the familiar apply function.

data class User(var username: String, val id: Int)
val user = User("Kris Kringle", 1234)
user.apply {
    username = "Santa"
}

With apply you don't have to reference the object you are working with, and instead, you can use properties such as username directly. Cool! So how can we implement our own apply?

Lambdas with receiver types is the answer!

This post assumes basic knowledge about lambdas. Head over to On wavelength with lambdas for a short introduction.

fun caps(user: User, text: User.() -> String): String = user.text().capitalize()

val user = User(username = "Santa", id = 1)
caps(user) {
    "$username!"
} // result: SANTA!

Ok, so let's take a closer look at whats happening in the example above. The caps function takes in a user and a lambda with some unusual syntax: text: User.() -> String. What's happening here? We have a lambda function type prefixed with a type! The prefix is a receiver type, and in our example that is a User.

But what does this mean? What is a receiver? Receivers assign a scope, or a receiver type, to a lambda. This means that we can change what this is inside a lambda. Having a User as a receiver gives us the power to call everything on a user object without having to explicitly reference it.

fun caps(user: User, text: User.() -> String) =
       user.text().capitalize()

The second thing that might look slightly weird at first glimpse is the way the text lambda is invoked. It's called directly on the user object as if it were an extension function. When working with lambdas with receivers we have some nice syntactical sugar; since the parameter user has the same type as the receiver, we can call the lambda directly on it. This means that in the context of our lambda user.text() is the same thing as text(user).

Our caps function doesn't quite look like apply yet, but we are getting there. On the other hand caps looks very similar to the with function in Kotlin, which is not a keyword, but is in fact a function in the standard library. Let's compare them.

caps(user) {
    "$username!"
}

with(user) {
    "$username!"
}

Both caps and with changes the context of the code block allowing us to refer directly to properties. But our goal was to implement apply, not with.

Extension functions to the rescue! Let's change our caps function to be an extension functions and see what happens.

fun User.caps(text: User.() -> String): String = text().capitalize()

In extension functions this refers to the object it's invoked on and since we are extending User, this is actually a reference to a user. When using receivers types this allows us to call text() directly without supplying the user to it.

fun User.caps(text: User.() -> String): String = text().capitalize()

val user = User(username = "Santa", id = 1)
user.caps {
    username
}

Now we're talking. With that change our caps function look and behave much more like apply. Using extension functions and lambdas with receiver types we can make a lot of cool code snippets, and even create amazing domain specific languages!

Receivers make it possible to create some pretty interesting things, but remember that with great syntactical powers comes great responsibilities.

Up next...

Loading…