Scala merge two maps with different value type

Viewed 137

Given two maps

  val m1 = Map("a" -> "a")
  val m2 = Map("a" -> 1)

as a result of merging those two, I would like to get

Map("a" -> ("a",1))

I tried cats semigroupal for this and it pretty much did the job, but I had to create instance for semigroupal by my self, like this

Semigroupal.catsSemigroupalForMap[String]

and then merge them with product method. What do I need to do to not create it manually? E.g. merging two options is straightforward

val noneInt: Option[Int] = None
val some3: Option[Int] = Some(3)
Semigroupal[Option].product(noneInt, some)

I would like to achieve a similar code to merge two maps, but when defining

Semigroupal[Map]

The compiler cannot find any implicity for it.

3 Answers

tupled seems to do the trick:

@ val mapA = Map(1 -> 2)
mapA: Map[Int, Int] = Map(1 -> 2)

@ val mapB = Map(1 -> "b")
mapB: Map[Int, String] = Map(1 -> "b")

@ (mapA, mapB).tupled
res4: Map[Int, (Int, String)] = Map(1 -> (2, "b"))

And it compiles generically:

@ def mergeMaps[A, B, C](m1: Map[A, B], m2: Map[A, C]): Map[A, (B, C)] = (m1, m2).tupled
defined function mergeMaps

@ mergeMaps(mapA, mapB)
res6: Map[Int, (Int, String)] = Map(1 -> (2, "b"))

It destroys keys that are not in both maps:

@ val m1 = Map("a" -> "foo", "b" -> "bar")
m1: Map[String, String] = Map("a" -> "foo", "b" -> "bar")

@ val m2 = Map("a" -> 1, "c" -> 3)
m2: Map[String, Int] = Map("a" -> 1, "c" -> 3)

@ mergeMaps(m1, m2)
res9: Map[String, (String, Int)] = Map("a" -> ("foo", 1))

This seems to be a good use case for Align

import cats.data.Ior
import cats.syntax.all._

def combineMaps[K, A, B](m1: Map[K, A], m2: Map[K, B]): Map[K, (A, B)] =
  m1.align(m2).collect {
    case (key, Ior.Both(a, b)) => key -> (a, b)
  }

Which can be used like this:

val m1 = Map("a" -> "foo", "b" -> "bar")
val m2 = Map("a" -> 1, "c" -> 3)

val result = combineMaps(m1, m2)
// result: Map[String, (String, Int)] = Map("a" -> ("foo", 1))

You can see the code running here.

Vanilla Scala may serve you as well. To merge multiple Maps you need to:

  • transform input Maps into sequences of tuples and append everything
  • groupBy to get into a Map again. This will accumulate the values from the same key.
  • pick whatever you want from the output Map
def merge(first: Map[String, Int], rest: Map[String, Int]*): Map[String, Seq[Int]] =
  rest
    .iterator
    .foldLeft(first.toSeq) { _ ++ _ } 
    .groupBy(_._1)
    .mapValues(pairs => pairs.map(_._2))

val m1 = Map("b" -> 2)
val m2 = Map("a" -> 1)
val m3 = Map("a" -> 1)

merge(m1, m2, m3)
merge(m1, m2)
merge(m1)

Related