'fold/reduce with complex accumulator

I have a list that looks like this:

val myList = listOf(
    Message(
      id= 1,
      info = listOf(1, 2)
    ),
    Message(
      id= 1,
      info = listOf(3, 4)
    ),
    Message(
      id= 2,
      info = listOf(5, 6)
    ) 
)

How can I convert it so the elements with the same id are combined?

listOf(
    Message
      id= 1
      info = listOf(1, 2, 3, 4)
    ),
    Message
      id= 2
      info = listOf(5, 6)
    ) 
)

I've tried the following, and it works


myList
    .groupBy { it.id }   
    .map { entry ->
        val infos = entry.value.fold(listOf<Int>()) { acc, e -> acc + e.info }

        Message(
            id = entry.key,
            info = infos
        )
    }

But I was wondering if there was an easier/cleaner/more idiomatic way to merge these objects. It seems like I would be able to do this with a single fold, but I can't wrap my brain around it.

Thanks



Solution 1:[1]

Would also go for groupingBy but do it a bit differently via fold (compare also Grouping):

myList.groupingBy { it.id }
      .fold({ _, _ -> mutableListOf<Int>() }) { _, acc, el ->
        acc.also { it += el.info }
      }
      .map { (id, infos) -> Message(id, infos) }

This way you have only 1 intermediate map and only 1 intermediate list per key, which accumulates your values. At the end you transform it in the form you require (e.g. into a Message). Maybe you do not even need that? Maybe the map is already what you are after?

In that case you may want to use something as follows (i.e. narrowing the mutable list type of the values):

val groupedMessages : Map<Int, List<Int>> = myList.groupingBy { it.id }
    .fold({ _, _ -> mutableListOf() }) { _, acc, el ->
      acc.also { it += el.info }
    }

Solution 2:[2]

You can groupingBy the ids, then reduce, which would perform a reduction on each of the groups.

myList.groupingBy { it.id }.reduce { id, acc, msg -> 
    Message(id, acc.info + msg.info) 
}.values

This will of course create lots of Message and List objects, but that's the way it is, since both are immutable. But there is also a chance that this doesn't matter in the grand scheme of things.

If you had a MutableMessage like this:

data class MutableMessage(
    val id: Int,
    val info: MutableList<Int>
)

You could do:

myList.groupingBy { it.id }.reduce { _, acc, msg ->
    acc.also { it.info.addAll(msg.info) }
}.values

Solution 3:[3]

A solution without using reduce or fold:

data class Message(val id: Int, val info: List<Int>)

val list = listOf(
  Message(id = 1, info = listOf(1, 2)),
  Message(id = 1, info = listOf(3, 4)),
  Message(id = 2, info = listOf(5, 6))
)

val result = list
  .groupBy { message -> message.id }
  .map { (_, message) -> message.first().copy(info = message.map { it.info }.flatten() ) }

result.forEach(::println)

Solution 4:[4]

By extracting out a few functions which have a meaning of their own, You can make it readable to a great extent.

data class Message(val id: Int, val info: List<Int>) {
    fun merge(that: Message): Message = this.copy(info = this.info + that.info)
}

fun List<Message>.mergeAll() =
    this.reduce { first, second -> first.merge(second) }

fun main() {
    val myList = listOf(
        Message(
            id = 1,
            info = listOf(1, 2)
        ),
        Message(
            id = 1,
            info = listOf(3, 4)
        ),
        Message(
            id = 2,
            info = listOf(5, 6)
        )
    )

    val output = myList
        .groupBy { it.id }
        .values
        .map { it.mergeAll() }

    println(output)
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2
Solution 3 lukas.j
Solution 4 Prateek Jain