Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Group values by a key with any Monoid

I would like to write a method mergeKeys that groups the values in an Iterable[(K, V)] by the keys. For example, I could write:

  def mergeKeysList[K, V](iter: Iterable[(K, V)]) = {
     iter.foldLeft(Map[K, List[V]]().withDefaultValue(List.empty[V])) {
        case (map, (k, v)) =>
          map + (k -> (v :: map(k)))
     }
  }

However, I would like to be able to use any Monoid instead of writing a method for List. For example, the values may be integers and I want to sum them instead of appending them in a list. Or they may be tuples (String, Int) where I want to accumulate the strings in a set but add the integers. How can I write such a method? Or is there something else I can use in scalaz to get this done?

Update: I wasn't as far away as I thought. I got a little bit closer, but I still don't know how to make it work if the values are tuples. Do I need to write yet another implicit conversion? I.e., one implicit conversion for each number of type parameters?

sealed trait SuperTraversable[T, U, F[_]]
extends scalaz.PimpedType[TraversableOnce[(T, F[U])]] {
  def mergeKeys(implicit mon: Monoid[F[U]]): Map[T, F[U]] = {
    value.foldLeft(Map[T, F[U]]().withDefaultValue(mon.zero)) {
      case (map, (k, v)) =>
        map + (k -> (map(k) |+| v))
    }
  }
}

implicit def superTraversable[T, U, F[_]](
  as: TraversableOnce[(T, F[U])]
): SuperTraversable[T, U, F] = 
    new SuperTraversable[T, U, F] {
      val value = as
    }
like image 305
schmmd Avatar asked Mar 15 '12 03:03

schmmd


1 Answers

First, while it's not relevant to your question, you are limiting your code's generality by explicitly mentioning the type constructor F[_]. It works fine without doing so:

sealed trait SuperTraversable[K, V]
extends scalaz.PimpedType[TraversableOnce[(K, V)]] {
    def mergeKeys(implicit mon: Monoid[V]): Map[K, V] = {
        value.foldLeft(Map[K, V]().withDefaultValue(mon.zero)) {
            case (map, (k, v)) =>
                map + (k -> (map(k) |+| v))
        }
    }
}

[...]

Now, for your actual question, there's no need to change mergeKeys to handle funny kinds of combinations; just write a Monoid to handle whatever kind of combining you want to do. Say you wanted to do your Strings+Ints example:

implicit def monoidStringInt = new Monoid[(String, Int)] {
    val zero = ("", 0)
    def append(a: (String, Int), b: => (String, Int)) = (a, b) match {
        case ((a1, a2), (b1, b2)) => (a1 + b1, a2 + b2)
    }
}

println {
    List(
        "a" -> ("Hello, ", 20),
        "b" -> ("Goodbye, ", 30),
        "a" -> ("World", 12)
    ).mergeKeys
}

gives

Map(a -> (Hello, World,32), b -> (Goodbye, ,30))
like image 84
Owen Avatar answered Nov 16 '22 15:11

Owen