1

I would like to create a union of two maps whose key type is the same and whose value type is a collection of elements, but whose types are different.

Consider the following contrived example:

case class Child(name: String)
val peopleToChildren: Map[String, Seq[Child]] = 
  Map("max" -> Seq(Child("a"), Child("b")), 
    "yaneeve" -> Seq(Child("y"), Child("d")))

case class Pet(name: String)
val peopleToPets: Map[String, Seq[Pet]] = 
  Map("max" -> Seq(Pet("fifi")), 
    "jill" -> Seq(Pet("bobo"), Pet("jack"), Pet("Roger rabbit")))

val peopleToChildrenAndDogs: Map[String, (Seq[Child], Seq[Pet])] = {
  // people may have children
  // people may have pets
  // would like a map from people to a tuple with a potentially empty list of children and a
  //     potentially empty list of pets
  // ???
}

What would be a way to do it which is concise, idiomatic, but still legible?

I found no single function that can do that in the standard scala collections library.

Proposed solutions can be based solely on the standard library, or propose an external solution.

I post it here since I could not easily find an online solution to a seemingly standard operation.

Yaneeve
  • 4,751
  • 10
  • 49
  • 87

3 Answers3

6

This appears to work.

val peopleToChildrenAndDogs: Map[String, (Seq[Child], Seq[Pet])] = {
  (peopleToChildren.keySet ++ peopleToPets.keySet).map { k =>
    k -> (peopleToChildren.getOrElse(k, Seq())
         ,peopleToPets.getOrElse(k, Seq()))
  }.toMap
}

Get all the keys. For every key do a getOrElse() on each of the feeder Maps.

jwvh
  • 50,871
  • 7
  • 38
  • 64
  • as cool as @OlegPyzhcov's solution is, and it is, I ended up using this version since it had no external deps and is concise – Yaneeve Sep 17 '17 at 06:00
5

Just for the curious, here's how it could be done using Scalaz:

import scalaz._, Scalaz._

case class Child(name: String)

val peopleToChildren = Map(
  "max"     -> List(Child("a"), Child("b")), 
  "yaneeve" -> List(Child("y"), Child("d"))
)

case class Pet(name: String)

val peopleToPets = Map(
  "max"  -> List(Pet("fifi")), 
  "jill" -> List(Pet("bobo"), Pet("jack"), Pet("Roger rabbit"))
)

val peopleToChildrenAndPets: Map[String, (List[Child], List[Pet])] = 
  peopleToChildren.strengthR(nil[Pet]) |+| peopleToPets.strengthL(nil[Child])

Explanation:

  • nil[Pet] is just an alias for List.empty[Pet]
  • strengthR for a given Functor tuples contained values, so that its parameter is at the right. Here it is equivalent to peopleToChildren.mapValues(v => (v, nil[Pet]))
  • strengthL is the same, but element will be added to the left
  • |+| is an append operator for a given Semigroup. The one here is derived recursively:
    • for Map[K, V], it uses |+| to combine values of type V if a given key exists in both Maps. If the value is only present in one of them, it will be retained as is. Here, V = (List[Child], List[Pet])
    • for tuples (A, B), it again uses |+| to combine both As and Bs. Here, A = List[Child] and B = List[Pet]
    • for lists of any type (as well as strings, vectors or streams) it does concatenation. This is why I had to change type of Map values to be Lists - for generic Seqs this operation is not defined

Result:

peopleToChildrenAndPets: Map[String, (List[Child], List[Pet])] = Map(
  "max" -> (List(Child("a"), Child("b")), List(Pet("fifi"))),
  "jill" -> (
    List(),
    List(Pet("bobo"), Pet("jack"), Pet("Roger rabbit"))
  ),
  "yaneeve" -> (List(Child("y"), Child("d")), List())
)
Oleg Pyzhcov
  • 7,323
  • 1
  • 18
  • 30
  • Really cool! Thank you for the in depth explanation! I wonder, also, how that would be done with cats seems there should be a straightforward transformation for this code – Yaneeve Sep 14 '17 at 11:02
  • @Yaneeve `strengthL` is `tupleLeft`, `strengthR` is `tupleRight`, `nil[...]` does not exists, so use `List.empty[...]` or `List[...]()` and imports are `import cats._, implicits._`. Might be broken on 1.0.0-MF tho, so try 0.9.0 if any problems pop up. – Oleg Pyzhcov Sep 14 '17 at 11:13
0

To answer my own question, the following is the way that I solved it, but it seems overly long and complex:

Welcome to the Ammonite Repl 1.0.2
(Scala 2.11.11 Java 1.8.0_91)
If you like Ammonite, please support our development at www.patreon.com/lihaoyi
@ case class Child(name: String)
defined class Child

@ val peopleToChildren: Map[String, Seq[Child]] =
    Map("max" -> Seq(Child("a"), Child("b")),
      "yaneeve" -> Seq(Child("y"), Child("d")))
peopleToChildren: Map[String, Seq[Child]] = Map("max" -> List(Child("a"), Child("b")), "yaneeve" -> List(Child("y"), Child("d")))

@

@ case class Pet(name: String)
defined class Pet

@ val peopleToPets: Map[String, Seq[Pet]] =
    Map("max" -> Seq(Pet("fifi")),
      "jill" -> Seq(Pet("bobo"), Pet("jack"), Pet("Roger rabbit")))
peopleToPets: Map[String, Seq[Pet]] = Map("max" -> List(Pet("fifi")), "jill" -> List(Pet("bobo"), Pet("jack"), Pet("Roger rabbit")))

@

@ val peopleToChildrenAndDogs: Map[String, (Seq[Child], Seq[Pet])] = {
    // people may have children
    // people may have pets
    // would like a map from people to a tuple with a potentially empty list of children and a
    //     potentially empty list of pets

    val paddedPeopleToChildren =  peopleToChildren.map{ case (person, children) => person -> (children, List.empty[Pet])}
    val paddedPeopleToPets = peopleToPets.map{ case (person, pets) => person ->(List.empty[Child], pets)}
    val notGoodEnough = paddedPeopleToPets ++ paddedPeopleToChildren // this is here to show that it does not work since it overwrites the value of a key - Map(max -> (List(Child(a), Child(b)),List()), jill -> (List(),List(Pet(bobo), Pet(jack), Pet(Roger rabbit))), yaneeve -> (List(Child(y), Child(d)),List()))

    val allSeq = paddedPeopleToPets.toSeq ++ paddedPeopleToChildren.toSeq
    val grouped = allSeq.groupBy(_._1).mapValues(_.map { case (_, tup) => tup })
    val solution = grouped.mapValues(_.unzip).mapValues {case (wrappedChildren, wrappedPets) => (wrappedChildren.flatten, wrappedPets.flatten)}
    solution
  }
peopleToChildrenAndDogs: Map[String, (Seq[Child], Seq[Pet])] = Map(
  "yaneeve" -> (ArrayBuffer(Child("y"), Child("d")), ArrayBuffer()),
  "max" -> (ArrayBuffer(Child("a"), Child("b")), ArrayBuffer(Pet("fifi"))),
  "jill" -> (ArrayBuffer(), ArrayBuffer(Pet("bobo"), Pet("jack"), Pet("Roger rabbit")))
)
Yaneeve
  • 4,751
  • 10
  • 49
  • 87