Don’t apologize! It’s a very good question about generics. The first function basically tells you that the function will return the same type as the input iterable, and so, barring any reflection craziness, it will return the input iterable, perhaps modified with some other type-perserving Iterable functions. The second function just tells you that it’ll return an iterable, which could be a completely different type from the input iterable. For all you know, the function could return you a Set or a List or anything else.
fun <S, T : S> Iterable<T>.reduce1(operation: (acc: S, T) -> S): S {
val iterator = this.iterator()
if (!iterator.hasNext()) throw UnsupportedOperationException("Empty collection can't be reduced.")
var accumulator: S = iterator.next()
while (iterator.hasNext()) {
accumulator = operation(accumulator, iterator.next())
}
return accumulator
}
fun <T> Iterable<T>.reduce2(operation: (acc: T, T) -> T): T {
val iterator = this.iterator()
if (!iterator.hasNext()) throw UnsupportedOperationException("Empty collection can't be reduced.")
var accumulator: T = iterator.next()
while (iterator.hasNext()) {
accumulator = operation(accumulator, iterator.next())
}
return accumulator
}
Similarly:
listOf(1, 2, 3).apply{
val r1a: Int = reduce1 { acc, it -> acc + it }
val r2a: Int = reduce2 { acc, it -> acc + it }
val r1b: Number = reduce1 { acc, it -> acc + it }
val r2b: Number = reduce2 { acc, it -> acc + it }
}
It does seem the same this time…
I don’t understand why we are allowed to up-cast the T elements to type S in the operation function when we could do the same to the result afterwards
There’s no need for the T:S constraint basically. In your example, both T and S are always Int, in fact, r1b and r2b only upcast at the end. It’s important to note that reduce doesn’t actually require that the acc and it share some common type. for instance:
listOf("hello", "world").reduce(0) { acc, it -> acc + it.length }
your version of reduce (both 1 and 2) assume that they have to share some common type or be the same type. Here’s an example of why reduce1 might be preferable to reduce2 in some situations:
open class Foo
class Bar: Foo()
fun Bar.combine(foo: Foo): Foo = TODO()
fun main() {
val foo = listOf(Bar()).reduce1 { acc: Foo, it: Bar -> it.combine(acc) }
val foo2 = listOf(Bar()).reduce2 { acc: Foo, it: Bar -> it.combine(acc) } // Error
}
As you can see, it allows us to return a supertype of Bar as the accumulator, while return2 forces us to return a Bar, unless we pretend that the list is actually a List<Foo>, but then we lose the ability of knowing that it: Bar and calling Bar specific operations on it.
To clear my thoughts, here is how we could come up with the function signature of reduce:
reduce is an extension function on Iterable<T>.
By design, reduce “Accumulates value starting with the first element and applying operation from left to right to current accumulator value and each element”.
From “Accumulates value starting with the first element”, reduce has a mutating variable accumulator (with placeholder type AccumulatorType).
accumulator must hold the first element T initially.
Therefore AccumulatorType is a type between T and Any?.
From “… applying operation from left to right to current accumulator value and each element”, expressed in pseudo code:
FOR EACH element IN iterable {
accumulator = operation(accumulator, element)
}
The operation function accepts accumulator and element, and returns accumulator.
Therefore the operation function has signature (AccumulatorType, T) -> AccumulatorType.
reduce returns accumulator
Combining , , : the function has signature:
fun <T: Any?> Iterable<T>.reduce(operation: (AccumulatorType, T) -> AccumulatorType): AccumulatorType
Then we attempt to combine the above with , we have to express the class hierarchy below in kotlin notation: