Not sure this is the right channel, but what would...
# language-proposals
j
Not sure this is the right channel, but what would need to happen for sumOf to be able to work on anything with a plus operator?
e
"has a plus operator" is not a type, especially since they're defined as extensions in many cases. you'd need one of • Self types and extension protocols like Swift; I think this is incompatible with JVM • structural types like Scala; this doesn't work with extensions and Scala implements it with reflection behind the scenes which isn't appropriate for many uses • typeclasses/traits like Haskell or Rust. this seems potentially doable, since a hypothetical
Copy code
trait Add<T> { type Output; operator fun plus(T): Output }
fun <T, R : Add<R>> Iterable<T>.sumOf(selector: (T) -> R): R where Add<T>::Output : R
is really just implemented as
Copy code
fun <T, R> Iterable.sumOf(dictionary: { plus: R.(R) -> R }, selector: (T) -> R): R
which you can write by hand now, so it doesn't feel like something we're really missing right now, at least not enough to work through all the design considerations (including Java interop), IMO
🙏 1
y
Not the right channel I don't think, but since the discussion is here already, here's my two cents: You can (finally) do something very close to this right now with context receivers. Here's a very small example:
Copy code
interface Add<T> {
    operator fun T.plus(other: T): T
}

object IntAdd : Add<Int> {
    override fun Int.plus(other: Int): Int {
        return this + other // This actually calls the member Int.plus
    }
}

context(Add<R>)
fun <T, R> Iterable<T>.sumOf(selector: (T) -> R): R? {
    var sum: R? = null
    for (element in this) {
        sum = if (sum == null) {
            selector(element)
        } else {
            sum + selector(element)
        }
    }
    return sum
}
context(Add<T>)
fun <T> Iterable<T>.sum(): T? {
    return sumOf { it }
}

fun test(){
    with(IntAdd){
        // Because of the context, this takes priority over the
        // context-less extensions defined in stdlib
        println(listOf(40, 42, 44).sum()!! / 3)
        println(listOf("hello", "test", "elo mate").sumOf(String::length))
    }
}
I used null to indicate that an Iterable is empty, but in fact, you can define a
Monoid
(fancy FP talk for an operation similar to plus or multiply alongside an identity element) and return that identity element if the list is empty.
🙏 1
e
yeah, that
Add<T>
is effectively the
dictionary
I stated. you can do that "by hand" in a variety of ways, including context receivers for nice syntax, so I don't see a strong demand for the compiler to handle typeclasses itself and generate/consume those tables itself (which is what Haskell does behind the scenes, when it can't be optimized out; Rust relies more on monomorphic specialization, which is not how JVM generics work)
y
There are a couple of issues with the context receivers approach. For one, defining a generic
ListAdd
requires you to have an object for each different type of list (i.e. requires a
ListIntAdd
,
ListStringAdd
etc.). You can sort of fix this by having
plus
accept a type parameter that extends
T
so that it returns the correct type. I'll include an example of that at the end of this reply. It has some dangerous consequences though because it has to unsafely cast to that subtype of
T
, which can be
ArrayList
if we're talking about lists, even though the implementation might not actually return an
ArrayList
. You could go with the initial design and use an
object ListAdd
with a
fun listAdd<T>() : Add<List<T>> = ListAdd as Add<List<T>>
, but you'd still have to include that context for every type of List that you use. This brings me onto the second issue: while including context receivers like that is explicit, it is also so horribly unelegant, especially with the List issue where you need a context for each concrete List<T>. That's why I'd tend to disagree with @ephemient here in that I think the compiler should have knowledge of which contexts are globally available and which contexts are available in the presence of other contexts (with some explicit syntax obviously). My vision would be that the compiler would allow us to write:
Copy code
context-provider-magic-syntax fun <T> listAdd(): Add<List<T>> = ListAdd as Add<List<T>>
and as a result whenever a
Add<List<whatever>>
is needed, it would be included as a context receiver in the local scope (just like the
with SomeObject
syntax that the KEEP suggests) Here's the hacky implementation for lists by using type parameter subtyping
e
I don't think Kotlin should never try, but I do think that we should work through all the ramifications, starting with the manual approach, before adding it to the language, to avoid things designing something that ends up like Haskell's and Scala's implicit parameters, both of which are largely considered to be mistakes
1
d
What is
dictionary: { plus: R.(R) -> R }
?
e
some hypothetical compiler-generated version of what Youssef manually wrote as
Add<T>
the whole dictionary-passing approach is well-known: https://okmij.org/ftp/Computation/typeclass.html
d
Ok IIUC the dictionary keys are the names of methods defined on the type class, yes? In this case the dictionary has 1 key because the type class has one method?
e
yes, although it gets more complex if we allow higher-kinded types
d
Right so similar to a vtable. This is also how interface support in Go is implemented (https://github.com/teh-cmc/go-internals/blob/master/chapter2_interfaces/README.md#dynamic-dispatch)
e
many OO language runtimes use vtables, including JVM. the difference is that the dictionary is separated from the object identity, so it can be contextual
👍 1
d
Every problem can be solved by adding an additional layer of indirection 🙃
(and yeah, Go is not OO, interfaces are similar to implicit traits, so it's pretty much the same as what you described)