fold/reduce with complex accumulator

Viewed 149

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

4 Answers

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

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 }
    }

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)

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)
}
Related