Jonathan Ellis
05/27/2022, 7:03 PMephemient
05/28/2022, 6:20 AMtrait 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
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), IMOYoussef Shoaib [MOD]
05/28/2022, 4:39 PMinterface 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.ephemient
05/28/2022, 4:46 PMAdd<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)Youssef Shoaib [MOD]
05/28/2022, 5:08 PMListAdd
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:
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 subtypingephemient
05/28/2022, 8:34 PMDan Fingal-Surma
05/30/2022, 6:02 PMdictionary: { plus: R.(R) -> R }
?ephemient
05/30/2022, 6:04 PMAdd<T>
ephemient
05/30/2022, 6:05 PMDan Fingal-Surma
05/30/2022, 6:30 PMephemient
05/30/2022, 6:31 PMDan Fingal-Surma
05/30/2022, 7:03 PMephemient
05/30/2022, 7:06 PMDan Fingal-Surma
05/30/2022, 11:32 PMDan Fingal-Surma
05/30/2022, 11:36 PM