Toying with Kotlin’s context receivers

A simplified model sample

class AccountService {
fun transfer(tx: Transaction, vararg operations: () -> Unit) {
tx.start()
try {
operations.forEach { it.invoke() }
tx.commit()
} catch (e: Exception) {
tx.rollback()
}
}
}
val service = AccountService()
val transaction = Transaction()
val repo = AccountRepo()
service.transfer(
transaction,
{ repo.credit(account1, 10.5) },
{ repo.debit(account2, 10.5) }
)

Improving the code with extension functions

class AccountService {
fun Transaction.transfer(vararg operations: () -> Unit) {
start() // 1
try {
operations.forEach { it.invoke() }
commit() // 1
} catch (e: Exception) {
rollback() // 1
}
}
}
  1. Implicit this references the Transaction object
with(service) {                               // 1
transaction.transfer( // 2
{ repo.credit(account1, 10.5) },
{ repo.debit(account2, 10.5) }
)
}
  1. Bring the service instance in scope
  2. So it’s valid to call transfer on the transaction object
  • The raw number of characters typed is less — it’s more concise
  • The semantics is radically different, though. The new code means that in the context of an AccountService, we can call transfer() on an existing Transaction object.
with(transaction) {
service.transfer(
{ repo.credit(account1, 10.5) },
{ repo.debit(account2, 10.5) }
)
}

Context receivers to the rescue

context(Foo, Bar, Baz)
fun myfunction() {}
val foo = Foo()
val bar = Bar()
val baz = Baz()
with(foo) { // 1
with(bar) { // 2
with (baz) { // 3
myfunction() // 4
}
}
}
  1. Bring foo in scope
  2. Bring bar in scope
  3. Bring baz in scope
  4. Call the function
context(Foo, Bar, Baz)
fun myfunction() {
println(this@Foo)
println(this@Bar)
println(this@Baz)
}
class AccountService {
context(Transaction)
fun transfer(vararg operations: () -> Unit) {
start() // 1
try {
operations.forEach { it.invoke() }
commit() // 1
} catch (e: Exception) {
rollback() // 1
}
}
}
  1. Implicit this references the Transaction object. We don't need to qualify further with the class name as there's no other context object
with(transaction) {                               // 1
service.transfer( // 2
{ repo.credit(account1, 10.5) },
{ repo.debit(account2, 10.5) }
)
}
  1. Bring transaction in scope
  2. Use the transaction object in scope

Discussion

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store