<@U2E974ELT> Testing using context receivers for t...
# functional
m
@elizarov Testing using context receivers for type classes (as claimed in the KEEP should be possible?) Why isn't this working?:
Copy code
interface ServiceLocator<I> {
//    fun getRealImpl(): I
    fun getEmulatedImpl(): I
//    fun getStubbedImpl(): I
}

context(ServiceLocator<I>)
inline fun <reified I> locateEmulated(): I = getEmulatedImpl()

object StringServiceLocator : ServiceLocator<String> {
    override fun getEmulatedImpl(): String = "test"
}

object LongServiceLocator : ServiceLocator<Long> {
    override fun getEmulatedImpl(): Long = 5
}

object StringLongLocatorFactory: ServiceLocatorFactory {
    // In the future (scope properties): with val ...
    inline fun <reified I> locate(): I {
        with(LongServiceLocator) {
            with(StringServiceLocator) {
                return locateEmulated<I>()
            }
        }
    }
}
No required context receiver found: Cxt { context(ServiceLocator<I>) public inline fun <reified I> locateEmulated(): I defined in [...] in file ServiceLocatorFactory.kt[SimpleFunctionDescriptorImpl@1aa4cbc9] }
More concrete it works:
Copy code
with(StringServiceLocator) {
            with(LongServiceLocator) {
                val emulator = locateEmulated<String>()
            }
        }
but then it is not useful as I could just execute
StringServiceLocator.getEmulatedImpl()
manually...
What I would like to do is this though is something like:
Copy code
object StringLongLocatorFactory: ServiceLocatorFactory {
    with val stringLocator = StringServiceLocator
    with val longLocator LongServiceLocator

    inline fun <reified I> locate(): I {
        with(LongServiceLocator) {
            with(StringServiceLocator) {
                return locateEmulated<I>()
            }
        }
    }
}
or more compact if possible. And String/Long are just examples of more complex stateful service classes.
y
What happens if
I
was
Int
or
List<String>
or something? What behaviour do you expect to happen then?
m
I would expect a compiler error. Scala 3 version:
Copy code
trait ServiceLocator[I] {
//    def getRealImpl(): I
    def getEmulatedImpl: I
//    def getStubbedImpl(): I
}

def summonEmulatedImpl[I](using locator: ServiceLocator[I]): I = locator.getEmulatedImpl

given stringServiceLocator : ServiceLocator[String] with {
    def getEmulatedImpl: String = "test"
}

given longServiceLocator : ServiceLocator[Long] with {
    def getEmulatedImpl: Long = 5L
}

object StringLongLocatorFactory {
    def locateEmulated[I](using locator: ServiceLocator[I]): I = summonEmulatedImpl[I]
}

val stringRes: String = StringLongLocatorFactory.locateEmulated
println(stringRes)

val longRes = StringLongLocatorFactory.locateEmulated[Long]
println(longRes)
https://scastie.scala-lang.org/uRjg6FxGRLKjPt4ip7ORIA
Compile error:
Copy code
[error] ./ServiceLocatorFactory.sc:28:58: No given instance of type ServiceLocatorFactory.ServiceLocator[Int] was found for parameter locator of method locateEmulated in object StringLongLocatorFactory
[error] val intRes = StringLongLocatorFactory.locateEmulated[Int]
https://scastie.scala-lang.org/xQVxyhQQRl6QGITOZ43wxg
y
Kotlin doesn't have an equivalent to
given
, it only has an equivalent to
using
, which is
context
. What you can do is have ServiceLocatorFactory take a
ServiceLocator
parameter, but then I guess it won't really be a factory.
m
Well, "with" has the ambition to be the same it sounds like.
y
Not globally though, which is the difference. Scala has context resolution that works globally by looking through any imported givens you have and seeing if they work, Kotlin doesn't. In Kotlin, you have to manually get your instances in scope before you can use them. Also, an important nuance is that, because of how manual it is, Kotlin doesn't have an easy way of defining the equivalent of a
Comparator<List<E>>
that relies on having a
Comparator<E>
in scope, so if you define such a structure, you have to manually bring in an instance for
Comparator<List<String>>
,
Comparator<List<Int>>
and so on.
m
Well, I don't require it to be global. I would be halfway happy if the resolution of the context variables were only done inside
object StringLongLocatorFactory
but hopefully without a pyramid of `with`s, but either "with val", annotation on the class, or:
Copy code
inline fun <reified I> locate(): I {
    with(LongServiceLocator, StringServiceLocator) {
        return locateEmulated<I>()
    }
}
It looks like the problem is that is the resolving of the actual context variable is eager and can't be deferred to the actual calls to
locate()
even when using
reified
.
y
Yeah that's the thing, it is eager. To make it non-eager you can make locate itself take a context, but then the outside will have to provide it somehow. Your Scala example relies on that global resolution btw and not on the fact that it's reified. Basically, you can take some Scala code and make all the usings into contexts and change all the givens to withs and theoretically it should work
In other words, your Scala example is closer to this:
Copy code
context(ServiceLocator<I>)
inline fun <I> locate(): I {
    return locateEmulated<I>()
}
But the key difference is that, when locate is called, Scala can globally find the required ServiceLocator instance, while with Kotlin you'll need something like a multi-with like so:
Copy code
inline fun <A, B, R> with(a: A, b: B, block: context(A, B) () -> R): R = block(a, b)
m
Hmm, japp. So means it can't be done within the current Kotlin language design?
y
Sadly yeah context receivers are kinda limited right now. The Kotlin team doesn't want another Scala implicits situation basically, and sure while using/given is better, I think that the potential value in it is not worth it for the Kotlin team right now because it'll need a lot of development not just on the compiler but also on the tooling side. What you can do is maybe have a DSL-style thing like this, where it'll bring in a fixed set of contexts into scope:
Copy code
inline fun <R> serviceLocators(block: context(LongServiceLocator, StringServiceLocator) () -> R): R = block(LongServiceLocator, StringServiceLocator)
Maybe when decorators become a thing in Kotlin you could define the above function as a decorator, and then using it would be more idiomatic
m
Yeah, context could be used for dependencies, but then it would be a more manual when-block or reflection to choose the right requested.
u
Even if it worked, what advantage multiple receivers would bring if you have to write a chain of with?
I'm struggling to find a single case where multiple receivers can solve a problem that typeclass would solve.
m
I probably agree, but Kotlins aproach to typeclasses sounds to be through multiple receivers.
u
Sorry but I cannot understand what this phrase mean. I heard it several times but still no concrete example of how it is supposed to work.
I'm not against Context receivers, it just that they don't solve the problems a typeclass can solve. For example with a type class you can abstract over the sum operator. Or create better builders.
y
Copy code
interface Monoid<T> {
  val zero: T
  operator fun T.plus(other: T): T
}
object IntMonoid : Monoid<Int> {
  override val zero = 0
  override operator fun Int.plus(other: Int) = this + other
}
 
context(Monoid<T>)
fun List<T>.sum() = fold(zero) { acc, t -> acc + t }
 
//Multi-with can help here if you want a lot of monoids
fun main() = with(IntMonoid) {
  println(list(1, 2, 42).sum())
}
u
sorry
IntMonoid
where comes from?
y
Sorry, I changed the name midway through writing from Summable to Monoid. Fixed it now! Edit: it's not letting me edit it for some reason. Slack is having issues
u
thanks for the code!
y
It's not letting me edit any messages for some reason. Slack is having issues, but just rename
IntSummable: Summable<Int>
to
IntMonoid: Monoid<Int>
u
yes I can see it modified now
1 min after you wrote fixed it. 🙂
Slack async
I don't have time to reply right now, but I'll do later.
Copy code
interface Monoid<T> {
    val zero: T
    operator fun T.plus(other: T): T
}
object IntMonoid : Monoid<Int> {
    override val zero = 0
    override operator fun Int.plus(other: Int) = this + other
}

context(Monoid<T>)
fun <T> List<T>.sum() = fold(zero) { acc, t -> acc + t }

//Multi-with can help here if you want a lot of monoids
fun main() = with(IntMonoid) {
    println(listOf(1, 2, 42).sum().toString())
}
there were a couple of typos, this version compile
the first objection is that if you define the sum inside the Monoid interface:
Copy code
interface Monoid<T> {
    val zero: T
    operator fun T.plus(other: T): T

    fun List<T>.monoidSum() = fold(zero) { acc, t -> acc + t }
}

//Int
object IntMonoid : Monoid<Int> {
    override val zero = 0
    override operator fun Int.plus(other: Int) = this + other
}
it works directly without any need of context receiver:
Copy code
fun main() {
        println(
            listOf(1, 2, 42).monoidSum().toString()
        )
}
the second objection is more deep. Having to specify the monoid instance before using it, makes it much less useful of real typeclasses: I cannot write a function like:
Copy code
fun <T: exists Monoid<T>> reduce(l: List<T>): T = l.monoidSum()