Search code examples
kotlingenericsrecursionflattenkotlin-reified-type-parameters

How to recursively implement a deep flatten on Iterable?


Having seen flatten, I was looking for something that would be a deepFlatten, i.e., it would work with not only Iterable<Iterable<T>> (it's pretty much the same for Arrays, but let's focus on Iterable now for brevity), but also with Iterable<Iterable<Iterable<T>>>, Iterable<Iterable<Iterable<Iterable<T>>>> and so on...

Of course, the result would have to be List<T>, which the standard flatten() doesn't provide - it would return List<Iterable<T> (or a List with more nested Iterables).

I was trying to work with reified generics:

inline fun <reified E, T> Iterable<E>.deepFlatten(): List<T> = when(E::class) {
    Iterable<*>::class -> (this as Iterable<Iterable<*>>).flatten().deepFlatten()
    else -> flatten()
}

But this obviously is flooded with errors:

  • T seems pretty undeducible
  • You cannot have a ::class of an interface
  • You cannot recurse on an inline function

Are there any workarounds on the above problems? Or, even better, is there a cleaner approach to the problem?


To demonstrate an example for completeness, I'd like to be able to do:

fun main() {
    val data: List<List<List<Int>>> = listOf(
            listOf(listOf(1, 2, 3), listOf(5, 6), listOf(7)),
            listOf(listOf(8, 9), listOf(10, 11, 12, 13))
    )

    print(data.deepFlatten()) // 1 2 3 4 5 6 7 8 9 10 11 12 13
}

The depth of nested Iterables (they need not be the same type - it's important that they are generically Iterable) can vary.


Solution

  • fun <T> Iterable<*>.deepFlatten(): List<T> {
        val result = ArrayList<T>()
        for (element in this) {
            when (element) {
                is Iterable<*> -> result.addAll(element.deepFlatten())
                else -> result.add(element as T)
            }
        }
        return result
    }
    ...
    
    println(data.deepFlatten<Int>())
    

    You have to specify the type explicitly and you lose compile-time safety. But it can flatten lists of any nesting and with elements of different types ([1, "foo", [3, "bar"]] -> [ 1, "foo", 3, "bar"])

    I would prefer a different solution. Something like this:

    typealias It2<T> = Iterable<Iterable<T>>
    typealias It3<T> = Iterable<It2<T>>
    typealias It4<T> = Iterable<It3<T>>
    typealias It5<T> = Iterable<It4<T>>
    //etc...
    
    fun <T> It3<T>.flatten2(): List<T> = flatten().flatten()
    fun <T> It4<T>.flatten3(): List<T> = flatten2().flatten()
    fun <T> It5<T>.flatten4(): List<T> = flatten3().flatten()
    //etc...
    
    ...
    println(data.flatten2())