https://kotlinlang.org logo
#language-evolution
Title
# language-evolution
y

Youssef Shoaib [MOD]

05/22/2022, 1:53 PM
Not sure if this is a bug with context receivers or just type inference not being strong enough. It seems as though the choice of a specific context receiver doesn't influence the choice of another context receiver when it comes to type parameters. Reproducer (The code is absurd but I have a legitimate use case that doesn't surround collections):
Copy code
// Implementation really doesn't matter, neither do the specific collections.
context(Map<K, V>, Set<V>)
fun <K, V> K.checkInclusion(): Boolean = this@Map[this] in this@Set // Check if our key K has a value which is a member of a set of accepted values

context(Set<Int>, Set<String>, Map<List<String>, String>, Map<List<Int>, Int>) // The last receiver is not needed to reproduce
fun reproducer(){
    listOf("hello", "world").checkInclusion() // Error: Multiple arguments applicable for context receiver
}
However, if I just let IntelliJ insert explicit type arguments based on what it has inferred, everything compiles successfully:
Copy code
listOf("hello", "world").checkInclusion<List<String>, String>()
IntelliJ even greys out the type arguments, saying that they're redundant. Is this a bug? Should I file this?
e

elizarov

06/22/2022, 8:02 PM
This is done by design to make the context lookup algo fast in the compiler. What’s your legitimate use-case, though?
y

Youssef Shoaib [MOD]

06/22/2022, 8:41 PM
I was testing out a few ideas for implementing type-class-y things using context receivers. I made this implementation for
Show
from Haskell, but just renamed to `Display`:
Copy code
interface Display<in F, in A> {
    context(SingleDisplay<A>) fun display(f: F): String
}

typealias SingleDisplay<T> = Display<T, T>
// ---------------------- This implementation doesn't really matter for the error
object IntDisplay: SingleDisplay<Int> {
    context(SingleDisplay<Int>) override fun display(f: Int): String {
        return "Display Int $f"
    }
}

object StringDisplay: SingleDisplay<String> {
    context(SingleDisplay<String>) override fun display(f: String): String {
        return "Display String $f"
    }
}

class ListDisplay<E>: Display<List<E>, E>{
    context(SingleDisplay<E>) override fun display(f: List<E>): String {
        return f.joinToString(prefix = "Display List [", postfix = "]", separator = ", ") { display(it) }
    }
}
// ----------------------

context(Display<F, A>, SingleDisplay<A>)
@Suppress("SUBTYPING_BETWEEN_CONTEXT_RECEIVERS") // False warning I think
fun <F, A> F.display(): String = display(this)

context(SingleDisplay<Int>, SingleDisplay<String>, Display<List<String>, String>
fun reproducer(){
    listOf("hello", "world").display() // Error: Multiple arguments applicable for context receiver
}
I get the same error here. If I, however, make the type parameters for
Display
not
in
, the code compiles successfully. I think this probably is a legitimate use-case, don't you think? It is maybe pushing Kotlin's type system a bit to its limits, but I'd say that since specifying the type args fixes the issue, then I'd expect it to just work without explicitness.
e

elizarov

06/23/2022, 7:06 AM
I'm quite puzzled by this code. What's the real purpose of having this workaround:
Copy code
interface Display<in F, in A> {
    context(SingleDisplay<A>) fun display(f: F): String
}
The above
display
function is already declared as a member of
Display
. Why would it also need a context of
Display
being already declared in the context of
Display
? Anyway, it looks like we simply forgot to forbit this kind of declaration to prevent people from running into those complications in the first place https://youtrack.jetbrains.com/issue/KT-52919
y

Youssef Shoaib [MOD]

06/23/2022, 7:14 AM
The
context(SingleDisplay<A>)
is for a child display. For instance, ListDisplay therefore requires, in its context, a way to display its elements. A
ListDisplay<String>
requires a
SingleDisplay<String>
in its context if you would like to call display on it. Obviously, for the trivial case of `IntDisplay`or
StringDisplay
, no child display is required, but for a `ListDisplay<E>`or a
ResultDisplay<E>
or an
OptionalDisplay<E>
, a way to display E is required, which here is modelled by
SingleDisplay<E>
(which is just a type alias for
Display<E, E>
The implementation of
ListDisplay
should show pretty well why a child display is required. Specifically, it's used in the lambda passed to `joinToString`so that we can safely display the inner elements based on the arbitrary implementation provided by the user
And in fact, we could have
SingleDisplay
be a completely separate interface, and we'd still run into the same issue. I don't have a compiler in front of me right now, but SingleDisplay could be defined as this:
Copy code
//Instead of typealias SingleDisplay<A> = Display<A, A>
interface SingleDisplay<in A> {
    fun display(a: A): String
}
// And remove the context(SingleDisplay) from IntDisplay and StringDisplay, but keep it in ListDisplay
And it would still give the same error, because in reality the error comes from a ambiguity of whether
SingleDisplay<String>
or
SingleDisplay<Int>
is the right context for the call inside reproducer
For the practicality of it, it might look questionable right now, but the vision is that, with a compiler plugin, a ListDisplay would be created out of thin air, with the correct type argument (i.e. its element type) and therefore the user would be forced to provide some way to display the inner elements. Sort of similar to C++ concepts.
e

elizarov

06/23/2022, 7:23 AM
I'm not getting any errors if I declare it as a separate interface and remove redundant
context
clauses from the
SingleDisplay.display
function: https://gist.github.com/elizarov/1d92bff4723ea8d7bd138e46e46fc772
Ah. I see now. Changing
SingleDisplay<T>
to
SingleDisplay<in T>
brings the error back
y

Youssef Shoaib [MOD]

06/23/2022, 7:29 AM
Yep, and that's why it's reproducible with `Map`s and `Set`s. The
in
variance is not necessarily crucial for this example, but I can imagine that for a more complex inheritance chain (e.g. if we were displaying a sealed class hierarchy) that the variance would be convenient. I'm still convinced though that context receivers resolution should just "figure this out" and not consider it ambiguous even though I can use a few inelegant hacks to work around it.
I've figured it out. Added comment to the issue & closed it. The underlying reason is that
Display
type parameters are underconstrained. There is no relation between a collection and its element type, so there is no way for compiler to pick the correct context.
y

Youssef Shoaib [MOD]

06/24/2022, 7:57 PM
Yep, I've realized now that I've tinkered with this a bit more that it's a variance issue at heart 🤦. In fact, in both the
Map Set
example and the
Display
example the issue is due to carelessly using functions that have
UnsafeVariance
(in the case of
Set
,
Set.contains
has unsafe variance, and in the case of
Display
, I got no error for this but implicitly the
SingleDisplay<A>
context receiver is actually using the wrong variance, but I guess the checkers for that in the compiler aren't implemented yet). In fact, since in most Display implementations I would be unpacking items from some sort of container (List, Set, Result, etc.) and then passing them to the
SingleDisplay<A>
,
A
actually needs to be invariant because
display
takes
in
As (as objects inside
F
) and also `out`puts `A`s into the outside world (by calling
SingleDisplay<A>.display
). Making it invariant does seem to fix the issue, and in fact I think the safest option here is to make everything invariant and then carefully see if variance can be added without breaking inference. In fact, it looks like just making
A
invariant still lets code like this compile:
Copy code
context(SingleDisplay<CharSequence>, Display<List<CharSequence>, CharSequence>)
fun foo() {
    listOf("hello", "world").display()
}
Thanks, by the way, for taking the time to look at this issue. It's quite helpful and reassuring that even the lead language designer takes time to look through and respond to design questions so frequently! I really appreciate your help and time!
e

elizarov

06/27/2022, 9:37 AM
You are welcome!
19 Views