Using context receivers for dependency injection, ...
# language-evolution
j
Using context receivers for dependency injection, is that a valid use-case?
Copy code
interface Blah {
    suspend fun doSomething()
}
Copy code
class BlahImpl : Blah {

    context(Connection)
    suspend fun doSomething() { ... }

}
currently the above doesn't compile (and the compiler actually just outputs garbage without hinting that this is not allowed - KT-58442) unless you add the context receiver on the
doSomething()
in the interface as well
Copy code
interface Blah {
    context(connection)
    suspend fun doSomething()
}
which leads to my question, should the interface really know in what context(s) it will be used, doesn't that defeat the purpose of an interface? the actual context in which the implementation runs sounds like implementation detail that now pollutes the interface. maybe the same can be said for suspend, should that really be in the interface too? that being said, if it's not in the interface, then you would be forced to specify at the call-site which implementation you are referring to which would then either require a context or no context or to call it from a coroutineScope or just normally.
s
The fact that it doesn’t compile is unrelated to its use-cases but due to it still being experimental and not all edges are ironed out in the compiler. It’s perfect valid for an interface function to only be valid in a certain context. I.e Android
Fragment
or Ktor
PipelineContext
. In your example the context of the method is another
interface
so it’s still completely abstract. IMO this gives a “stronger” coupling to
Connection
or databases. If you want the interface to be more flexible then that
context
shouldn’t be there but it kind-of depends on your use-cases and requirements for the `interface`/contract
i
I think it works just as it should work. It must be possible to use any implementation whenever interface is used. If implementation additionally requires context, it cannot be used as an implementation for this interface.
Copy code
interface Blah {
    suspend fun doSomething()
}

class BlahImpl : Blah {

    context(Connection)
    suspend fun doSomething() { ... }

} 

suspend fun acceptsBlah(blah: Blah) {
    // if it's BlahImpl we must provide context, but it's unknown because we use interface
    doSomething()
}
y
You can have Blah take a type parameter
Ctx
and hence the requirement will be passed all the way up the call chain until the piece of code that knows that you use
BlahImpl
b
There's also the option of putting
context(Connection)
on the class, though for your use case I'm not sure that's what you'll want. But yeah, like Youssef said, the context on doSomething needs to be in the interface function's signature so they're the same signature. And if the Impl needs a specific kind of connection, make the interface 's connection generic
j
I had a bit of a think and context receivers seems like a less than optimal fit for doing "DI" considering you need to
context
the interface too. Delegates seem like the better choice
Copy code
interface Blah {
	fun doSomething()
}

class FakeBlah : Blah {
	override fun doSomething() {
		TODO("Not yet implemented")
	}

}

class RealBlah : Blah  {
	override fun doSomething() {
		TODO("Not yet implemented")
	}

}
Copy code
class BlahApp(blah: Blah): Blah by blah {
	constructor() : this(blah = RealBlah())
}
I can now have connection in the real implementation and no connection in the fake implementation
Copy code
class RealBlah(val connection: Connection) : Blah  {
	override fun doSomething() {
		// do something with connection
	}

}
seems cleaner than polluting the interface with a
context(Connection)
y
Same thing can be done using a
context(Connection) class RealBlah: Blah
, and the
BlahApp
can either propagate the context requirement up, or simply provide it there. The difference between the 2 is that by having
context
in the interface, you're taking in a
Connection
each time
doSomething
is called. This is useful if
Connection
is a value that changes often like a Transaction for instance. In the 2nd case,
Connection
is actually an implementation detail that needs to only be provided once, hence why you can just pass it in to the constructor, and again context receivers support that use case through context classes
j
yes, figured I can have the best of both worlds here
Copy code
interface Blah {
	fun doSomething()
}

class FakeBlah : Blah {
	override fun doSomething() {}
}

context (Connection)
class RealBlah() : Blah {
	override fun doSomething() {}
}

class BlahDelegate(blah: Blah) : Blah by blah

fun main() {

	BlahDelegate(blah = FakeBlah())
	
	with(Connection()) {
		BlahDelegate(blah = RealBlah())
	}
}
385 Views