class: center, middle, title-slide count: false # .grey[**Context receivers**] ## Kotlin's new secret sauce
.less-line-height[ Alejandro Serrano @ Kotlin Meetup XL .grey[๐ฆ @trupill - ๐โโฌ serras - ๐จโ๐ป 47 Degrees] ] --- # ๐งฉ Context receivers New Kotlin feature in 1.6.20 and 1.7 --- # ๐งฉ Context receivers **Coolest** Kotlin feature in 1.6.20 and 1.7 -- ## Lots of use cases
.grey[Cross-cutting concerns] ๐ชข Dependency injection
โฅ Nested configurations
๐งฌ Detached interfaces
โก Effect scopes
โฅ Describing typed errors --- # ๐ Logging Prime example of .grey[cross-cutting concern] -- - It's **never** part of the **main** domain - If explicit, it pollutes the code - We want to **hide** any boilerplate around it - Usually **re-used** across several domains or modules within the same application --- # ๐ Logging Prime example of .grey[cross-cutting concern] ```kotlin interface Logger { suspend fun log( level: LogLevel, subject: String, message: String ) } ``` -- .smaller[Why `suspend` in `log`? If it may do I/O, I always add it] --- # ๐งฉ Context receivers in use 1๏ธโฃ Add to the signature where needed .code70[ ```kotlin context(Logger) suspend fun downloadData(url: URL): Info? = ``` ] -- 2๏ธโฃ The interface is available in the body .code70[ ```kotlin request(url).also { when(it) { null -> log(LogLevel.CRITICAL, ..., ...) } }?.let { parseInfo(it) } ``` ] --- # ๐งฉ Context receivers in use 3๏ธโฃ You can call other functions with the same context without further boilerplate .code70[ ```kotlin context(Logger) suspend fun initializeApp(): App? { downloadData(App.DATA_URL)?.let { downloadAvatar(it.avatarImage) } } ``` ] --- # ๐งฉ Context receivers in use 4๏ธโฃ Inject โ provide an impl. โ using `with` .code70[ ```kotlin class ConsoleLogger(): Logger { ... } suspend fun main() { with(ConsoleLogger()) { config = initializeApp() ... } } ``` ] --- # ๐ฉ Implicit receivers ## What is `this`? -- Answers the need of implicit supporting values .code80[ ```kotlin class Database { // โ dispatch context(Logger) // โ context suspend fun User.save() { // โ extension // if you need to refer to one this@Logger.log(...) } } ``` ] --- # ๐ฉ Implicit receivers ## What is `this`? Answers the need of implicit supporting values .code80[ ```kotlin class Database { // โ dispatch context(Logger) // โ context suspend fun User.save() { // โ extension // in the body, this has type // Database & Logger & User } } ``` ] --- # ๐ฉ Implicit receivers ## What is `this`? Answers the need of implicit supporting values .code80[ ```kotlin class Database { // โ dispatch context(logger@Logger) // โ context suspend fun User.save() { // โ extension // you can use context tags logger.log(...) } } ``` ] --- # ๐ฉ Implicit receivers ## What is `this`? **Dispatch**: where the method is declared
non-existing for top-level functions **Extension**: introduce with `fun Type.name(...)`
adds the ability to call with `.` **Context**: extensions on steroids
values come from `with`
syntax with `.` not available --- class: center, middle, title-slide # ๐ชข Dependency injection --- # ๐ชข Dependency injection ## .grey[Basic use case for context receivers] .code70[ ```kotlin context(Logger, HttpService) fun login(usr: String, pass: String): User? { ... // compute password hash val params = mapOf("username" to usr, "password" to hash) val r = httpPost("/login", params).getOrNull() return r?.let { result -> log(LogLevel.INFO, "login", "successful") parseUser(result) } } ``` ] --- # ๐ชข Dependency injection ## .grey[Basic use case for context receivers] .margin-top[ ๐งถ Context declares the services you require
๐ชก `with` injects the values ] Those two rules provide many benefits
over dependency injection frameworks --- # ๐งถ Declare the services you require The required services become part of the function signature with `context` - The compiler can check good usage - The IDE can help you diagnosing problems - Explicit documentation about what the function may do --- # ๐งถ Declare the services you require Some contexts may depend on others .code70[ ```kotlin context(Logger) class NetworkHttpService(): HttpService { ... } ``` ] -- This imposes _ordering_ at injection time .code70[ ```kotlin with(ConsoleLogger()) { with(NetworkHttpService()) { ... } } ``` ] --- # ๐ชก Inject the values using `with`
.font150[ **`with` is just regular Kotlin!** ] This makes many patterns easier to achieve --- # ๐ชก Inject the values using `with` ## .grey[Hassle-free mocking] .code70[ ```kotlin class AlwaysFails : NetworkHttpService { override suspend fun httpPost(url: URL) = null } ``` Check your code without any actual connection ```kotlin with(AlwaysFails()) { // test that things work when network is down } ``` ] --- # ๐ชก Inject the values using `with` ## .grey[Override part of a service] We want to change the minimum logging level for some piece of code -- .code60[ ```kotlin class OnlyImportantLogger(val logger: Logger): Logger { override suspend fun log(lvl: LogLevel, sb: String, msg: String) = when (lvl) { LogLevel.INFO, LogLevel.WARN -> { } else -> logger.log(lvl, sb, msg) } } ``` ] --- # ๐ชก Inject the values using `with` ## .grey[Override part of a service] We want to change the minimum logging level for some piece of code .code70[ ```kotlin context(Logger) fun User.logOut() { ... // do the important stuff with(OnlyImportantLogger(this@Logger)) { ... // don't care if it fails } } ``` ] --- # ๐ชก Inject the values using `with` ## .grey[Combine services] We often want to put some services together - As a _module_, to alleviate boilerplate - At the _initialization_ step in your app. - Based on some configuration .code70[ ```kotlin data class AppModule( val logger: Logger, val http: HttpService ) ``` ] --- # ๐ชก Inject the values using `with` ## .grey[Combine services] .margin-top[ - At the _initialization_ step in your app. - Based on some configuration ] .code80[ ```kotlin suspend fun main() { val app = initialize() with(app.logger, app.http, ...) { // do the job } } ``` ] --- # ๐ชก Inject the values using `with` ## .grey[Combine services] using delegation .code70[ ```kotlin data class AppModule( val logger: Logger, val http: HttpService ): Logger by logger, HttpService by http ``` ] -- .code70[ ```kotlin suspend fun main() { val app = initialize() with(app) { // do the job } } ``` ] --- # ๐ชก Inject the values using `with` ## .grey[Combine services] using delegation .code70[ ```kotlin data class AppModule( val logger: Logger, val http: HttpService ): Logger by logger, HttpService by http ``` ] .code70[ ```kotlin suspend fun main() { with(initialize()) { // do the job } } ``` ] --- # ๐ชข Context receivers ## .grey[Solve the nested configuration problem] Boilerplate to handle services which depend on others, or not clear why it works .margin-top[ - Services that depend on others declare so - The compiler _enforces_ the ordering - No need to "extract" from a bigger context - Problematic with implicits / type classes ] --- class: center, middle, title-slide # ๐งฌ Detached interfaces # .grey[Type Classes] --- # โ๏ธ Comparable classes .font80[ > This interface imposes a total ordering on the objects of each class that implements it. This ordering is referred to as the class's _natural ordering_. ] .code60[ ```kotlin interface Comparable
{ fun compareTo(other: T): Int } fun
> T.greaterThan(other: T) = compareTo(other) > 0 ``` ] -- 1. What if you do **not control** the class? 2. Or there's **more than one** such ordering? --- # ๐งฌ Detached interfaces ## .grey[Require `Comparable` as part of the context] .code70[ ```kotlin interface Comparable
{ fun compareTo(one: T, other: T): Int } context(Comparable
) fun
T.greaterThan(other: T) = compareTo(this, other) > 0 ``` ] --- # ๐๏ธ Lack of control .margin-top[ - New serialization format - Different way to log things as strings ] You cannot make `Boolean` implement those!
(or any class you hadn't written) -- .code70[ ```kotlin val booleanPretty = object : Pretty
{ override fun Boolean.pretty() = if (this) "verdad" else "mentira" } ``` ] --- # ๐๏ธ Lack of control .code70[ ```kotlin val booleanPretty = object : Pretty
{ override fun Boolean.pretty() = if (this) "verdad" else "mentira" } ``` ] Annoying to use because of manual injection .code70[ ```kotlin with (booleanPretty, intPretty) { ... } ``` ] -- Haskell and Scala provide special syntax
Arrow developers are working on a solution! --- # ๐๏ธ Lack of control Arrow developers are working on a solution! ### .grey[Arrow Inject] (work in progress) .code70[ ```kotlin @Provider val booleanPretty = ... ``` Bring the `@Provider`s into scope ```kotlin contextual
{ ... } ``` ] --- # ๐ฑ There's more than one ordering .code70[ ```kotlin fun
> MutableList
.sort() ``` ] This cannot be re-used to sort - ... backwards - ... using a specific key We are tied to the "natural" ordering --- # ๐งโโ๏ธ The Comparator ## .grey[There's more than one ordering] .code70[ ```kotlin // does it ring a bell? interface Comparator
{ fun compare(a: T, b: T): Int } fun
MutableList
.sortWith( comparator: Comparator
) ``` ] We end up duplicating each function! --- # ๐ฑ There's more than one ordering .code50[ ```kotlin interface Comparable
{ fun compareTo(one: T, other: T): Int } ``` ] .code70[ ```kotlin context(Comparable
) fun
reversed() = object : Comparable
{ override fun compareTo(one: T, other: T): Int = this@Comparable.compareTo(one, other) } ``` ] -- Replace the ordering with the desired one .code70[ ```kotlin with (reversed()) { list.sort() } ``` ] --- # ๐งฌ Detached interfaces Useful for "adjectives" or "-ables" - Detach the interface from the class - Increased extensibility Similar to type classes in Haskell
and `given`s in Scala 3 --- class: center, middle, title-slide # โก Typed error scopes # .grey[Effects] --- # ๐ The error dichotomy ## Two ways to handle errors in Kotlin โก **Exceptions** - Untyped by default (not in Java) - No code if you are not handling them - Special syntax: `try`, `catch` --- # ๐ The error dichotomy ## Two ways to handle errors in Kotlin โก **Exceptions** ๐ท๏ธ **Tagged types** (`?`, `Result`, `Either`) - Explicitly typed - No special syntax, `when` is enough - Some amount of boilerplate --- # ๐ The error dichotomy ## What if we could have the best of both? โก **Exceptions** - No code if you are not handling them ๐ท๏ธ **Tagged types** (`?`, `Result`, `Either`) - .grey[**Explicitly typed**] - No special syntax, `when` is enough --- # โก Error scopes .font60[Arrow Core ๐ `arrow-kt.io`] ๐ท๏ธ Explicitly typed .code60[ .little-margin-top[ ```kotlin context(EffectScope
) suspend fun readFile(path: Path): Content { ... } ``` ] ] โก No code if you are not handling errors .code60[ .little-margin-top[ ```kotlin context(EffectScope
) suspend fun allFiles(vararg path: Path): List
= path.map { readFile(it) } ``` ] ] .font60[Example based on Simon Vergauwen's blog @ `nomisrev.github.io`] --- # โก Error scopes .font60[Arrow Core ๐ `arrow-kt.io`] ๐ท๏ธ Explicitly typed .code60[ ```kotlin sealed interface FileError data class FileDoesNotExist(val path: Path): FileError data class PermissionError(...): FileError context(EffectScope
) suspend fun readFile(path: Path): Content { if (!path.exists()) shift(FileDoesNotExist(path)) ... // keep going } ``` ] .font60[Example based on Simon Vergauwen's blog @ `nomisrev.github.io`] --- # โก Error scopes .font60[Arrow Core ๐ `arrow-kt.io`] ๐ท๏ธ No special syntax, `when` is enough .code70[ ```kotlin when (effect { allFiles(paths) }.orNull()) { null -> ... // something went wrong else -> ... // everything ok } ``` ] -- .code70[ ```kotlin when (effect { allFiles(paths) }.fold( { problem -> ... /* something went wrong */ }, { result -> ... /* everything ok */ } ) ``` ] --- # ๐ญ Everything is a context .code65[ ```kotlin fun CoroutineScope.launch( context: CoroutineContext = ..., start: CoroutineStart = ..., block: suspend CoroutineScope.() -> Unit ): Job ``` ] KotlinX Coroutines uses extension receivers --- # ๐ญ Everything is a context .code65[ ```kotlin context(CoroutineScope) fun launch( context: CoroutineContext = ..., start: CoroutineStart = ..., block: context(CoroutineScope) suspend () -> Unit ): Job ``` ] `CoroutineScope` provides a _service_ or _effect_ - Creation and mgmt. of concurrent jobs --- # ๐งฉ Summary .grey[**Context receivers**] are awesome ๐ชข _Dependency injection_
Checked by the compiler, easier modification ๐งฌ _Detached interfaces_
Enable new forms of extensibility โก _Effect scopes_
Simpler than `Either`, with the same guarantees --- class: center, middle, title-slide # ๐คฉ It's been a pleasure ## Enjoy the rest of the talks!