Context with receivers is a new experimental Kotlin feature. So let’s explore this feature a bit and see what it is all about.

So what are they? In essence they are just something we call implicit parameters. Basically, it is a function argument without it appearing in the function signature. It is best illustrated with an example.

class DatabaseConnection {
  fun query(q : String) : Unit = TODO() // Implementation does not matter for this example
}

context(DatabaseConnection) // Needs a Database connection to be present in the context
fun getUsers() {
  query("select * from users") // Calling a method available within the context
}


fun main(args: Array<String>) {
  with(DatabaseConnection()) { // Add a DatabaseConnection to the context
    getUsers() // We can now call getUsers
  }
  // Calling getUsers() here would not work
}

Here we see a function getUsers() that does not have any arguments. It does however have the new context keyword attached to it. This means this function needs to be called within the context of a DatabaseConnection. We can now call the methods of DatabaseConnection directly within getUsers().

In order to get the DatabaseConnection into context we can use the with statement and supply the DatabaseConnection. Now methods called within this with statement will have a DatabaseConnection in its context and can use it as such.

Because getUsers() needs to be executed within a context that has a DatabaseConnection, calling it without one would cause a compilation error. So it is impossible to call getUsers() unless it is wrapped within a with statement. This with statement can be higher up in the function chain as demonstrated here:

context(DatabaseConnection) // Needs a DatabaseConnection within context because getUsers() requires one
fun processUsers() {
  getUsers() // Calls getUsers
}

context(DatabaseConnection)
fun getUsers() {
  query("select * from users")
}

fun main(args: Array<String>) {
  with(DatabaseConnection()) {
    processUsers() // Call processUsers instead
  }
}

Here we see that processUsers() calls getUsers(). So processUsers() needs to supply getUsers() with a context. It can do that by adding a with statement, but it does not have to. It can also delegate this to the caller of processUsers() by demanding it needs to run in a context with DatabaseConnection itself. So as long as somewhere in the code there is a DatabaseConnection supplied in the context, these methods can be called and chained.

This code above is therefore functionally equivalent to this:

fun processUsers(dbc: DatabaseConnection) {
  getUsers(dbc)
}

fun getUsers(dbc: DatabaseConnection) {
  dbc.query("select * from users")
}

fun main(args: Array<String>) {
  processUsers(DatabaseConnection())
}

So why would you choose to use a context instead of just passing it as an argument? First of all, it can tidy up the code a bit if you have long function chains where the last function called requires something to be in context, and you do not want to pass this down as an argument to all other functions along the way. Secondly, sometimes you are not able to add arguments to a function. If you for example try to override an operator like +, you can only have 1 argument to this function. This new trick provides a way to implicitly add these extra arguments without them appearing in the function signature. If you are wondering why you should be using context receivers instead of extension functions (or both!), you should check out the blog that Nicolas wrote.

Further reading:

shadow-left