Explain the Element/Key model of CoroutineContext: how does context[CoroutineName] resolve an element, and why is each element also a CoroutineContext?
answer
- get / fold / plus / minusKey are the four operations
- Element extends CoroutineContext (one-element context)
- Key is usually the companion object
- context[CoroutineName] returns CoroutineName? type-safely
- One element per key; same key replaces
basics
~20 sEach setting in the context is an Element tagged with a unique Key. You look one up by indexing with its key. Every single element is itself a tiny one-item context, which is why you can add them together with +.
solid answer
~40 sCoroutineContext models a typed set indexed by Key<E>. Each Element implements CoroutineContext.Element and carries a key property of type Key<E>. The Key is usually the companion object of the element class — for instance CoroutineName.Companion is a Key<CoroutineName>, so context[CoroutineName] returns a CoroutineName? with the value's type recovered statically. Crucially, Element extends CoroutineContext, so a single element is a one-element context. That uniformity is what lets + combine elements and contexts uniformly (CoroutineContext.plus), and what lets get, minusKey, and fold be defined once on the interface. get walks the combined context to find the element whose key matches; because keys are identity-based singletons, lookups are O(small) and type-safe — no casting needed by callers.
code
kotlin · 13 linesimport kotlin.coroutines.*
// A custom context element carrying a request id
class RequestId(val value: String) : AbstractCoroutineContextElement(RequestId) {
companion object Key : CoroutineContext.Key<RequestId>
}
fun main() {
val ctx: CoroutineContext = RequestId("r-42") + EmptyCoroutineContext
println(ctx[RequestId]?.value) // r-42, statically typed as String
val without = ctx.minusKey(RequestId)
println(without[RequestId]) // null
}go deeper
Knows you look up elements with context[Key] and combine with +.
Explains Element/Key, the companion-as-key convention, and that get is type-safe via the key's generic.
Describes get/fold/plus/minusKey, CombinedContext/EmptyCoroutineContext internals, and same-key replacement semantics.
Designs custom elements/keys for cross-cutting concerns and reasons about the typed-set algebra vs. thread-locals.
## The four operations of CoroutineContext The `CoroutineContext` interface defines a tiny algebra: - `operator fun <E : Element> get(key: Key<E>): E?` — look up an element by key (used by the `context[Key]` indexing syntax). - `fun <R> fold(initial: R, operation: (R, Element) -> R): R` — reduce over all elements. - `operator fun plus(context: CoroutineContext): CoroutineContext` — combine two contexts. - `fun minusKey(key: Key<*>): CoroutineContext` — return a copy without the element for that key. ## Element and Key An **Element** is a single context entry. It implements `CoroutineContext.Element`, which **extends `CoroutineContext`** — so *every element is itself a valid one-element context*. Each element has: ```kotlin public interface Element : CoroutineContext { public val key: Key<*> // get/fold/plus/minusKey get sensible single-element defaults } ``` A **Key** is a marker object of type `Key<E>`. Conventionally the key is the element class's **companion object**: ```kotlin public data class CoroutineName(val name: String) : AbstractCoroutineContextElement(CoroutineName) { public companion object Key : CoroutineContext.Key<CoroutineName> } ``` Because `CoroutineName`'s companion is named `Key` and is a `Key<CoroutineName>`, writing `context[CoroutineName]` passes the companion as the key and statically returns `CoroutineName?`. No cast is needed: the generic `get(key: Key<E>): E?` recovers the precise type from the key. ## Why uniformity matters Because an element *is* a context, the same `+` works whether operands are single elements or large combined contexts: ```kotlin val a: CoroutineContext = CoroutineName("x") // element-as-context val b: CoroutineContext = a + Dispatchers.IO // CombinedContext val c: CoroutineContext = b.minusKey(CoroutineName) // drops the name val name = b[CoroutineName]?.name // "x" ``` Internally, combining produces a `CombinedContext` (a linked structure), and `EmptyCoroutineContext` is the identity element. `get` walks this structure comparing keys by identity. Because keys are singletons, **at most one element per key** can exist; adding a second element with the same key replaces the first. ## Custom elements You can define your own element by extending `AbstractCoroutineContextElement` and supplying a companion `Key`. Libraries use this to thread request-scoped data (e.g. a tenant id) through coroutines type-safely instead of using thread-locals.
- What happens if you add two elements with the same Key using +?The right-hand element replaces the left-hand one; a context holds at most one element per key.
- How does get return the correct type without a cast?get's signature get(key: Key<E>): E? ties the result type to the key's type parameter, so the compiler infers it from the key you pass.
saying these in an interview costs you the question
- Saying you can store multiple elements under one key
- Claiming you must cast the result of context[Key]
- Not knowing the key is conventionally the companion object
- Thinking + appends without dedup (it replaces same-key elements)
- Confusing fold (reduce over elements) with get (single lookup)