Differences in generic function signatures

Is there any behavior difference between these two functions?

inline fun <T, C : Iterable<T>> C.onEach1(action: (T) -> Unit): C {
    return apply { for (element in this) action(element) }
}

(onEach1 is the onEach function in stdlib 1.9.0)

inline fun <T> Iterable<T>.onEach2(action: (T) -> Unit): Iterable<T> {
    return apply { for (element in this) action(element) }
}

If they are the same, is one style preferred over the other?
thanks!

I figured it out sorry for the stupid question

listOf(1, 2, 3).apply{
    val oe1a: Iterable<Int> = onEach1 { println(it) }
    val oe2a: Iterable<Int> = onEach2 { println(it) }
    val oe1b: List<Int> = onEach1 { println(it) }

    //type mismatch
    /*val oe2b: List<Int> = onEach2 { println(it) }*/
}
1 Like

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.

Thanks for the encouragement :slightly_smiling_face:

Now I got another issue with reduce…

        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 :thinking:

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.

1 Like

yeah and reduce1 is the actual reduce function in the standard library of Kotlin 1.9.0

This is this missing piece of puzzle in my understanding, thanks again!

To clear my thoughts, here is how we could come up with the function signature of reduce:

  • :one: 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.
      • :two: 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.
      • :three: Therefore the operation function has signature (AccumulatorType, T) -> AccumulatorType.
    • :four: reduce returns accumulator

 

Combining :one:, :three:, :four:: the function has signature:

fun <T: Any?> Iterable<T>.reduce(operation: (AccumulatorType, T) -> AccumulatorType): AccumulatorType

 

Then we attempt to combine the above with :two:, we have to express the class hierarchy below in kotlin notation:

Nothing <= T <= AccumulatorType <= Any?

So we get:

fun <T: AccumulatorType, AccumulatorType: Any?> Iterable<T>.reduce(operation: (acc: AccumulatorType, T) -> AccumulatorType): AccumulatorType

 

Finally simplify the Any? upperbound with implicit upperbound, and substitute AccumulatorType with S, we get:

fun <T: S, S> Iterable<T>.reduce(operation: (acc: S, T) -> S): S

Which is the actual function signature of reduce in the standard library!

1 Like