Merge maps in scalaz with a complex (double) opera

2019-07-28 11:55发布

问题:

I am using a map to associate certain values with a tuple (Int, Double) where the int is the order they appeared and the double the number of times they show (it is not, but is clearer like this using int and double to distinguish)

The tricky part is that I want to use different monoids for each element of the tuple, for the int I want to keep the min value, to remember first appearance, while for the double I want to use the addition monoid So for an existing key we have:

val map1 = Map("a" -> (1, 5.0), "b" -> (2, 4.0), "c" -> (3, 8.0))
val map2 = Map("b" -> (4, 1.0))

val merge = map1.toMap |+| map2.toMap
// Map(a -> (1, 5.0), b -> (2, 5.0), c -> (3, 8.0))

And for a new key we have:

val map2 = Map("d" -> (4, 1.0))

val merge2 = map1.toMap |+| map2.toMap
// Map(a -> (1, 5.0), b -> (2, 4.0), c -> (3, 8.0), d -> (4, 1.0))

I can not find a way do this, i can obviously use the addition monoid, and i can use the minval one, but i can not see how to combine them. Any help appreciated! thanks

回答1:

You can use scalaz.std.tuple.tuple2Monoid explicitly with the two monoids you want:

import scalaz.Monoid

implicit val countMonoid: Monoid[(Int, Double)] = scalaz.std.tuple.tuple2Monoid(
  Monoid.instance[Int](math.min(_, _), Int.MaxValue),
  Monoid.instance[Double](_ + _, 0)
)

And then:

scala> import scalaz.std.map._, scalaz.syntax.monoid._
import scalaz.std.map._
import scalaz.syntax.monoid._

scala> val map1 = Map("a" -> (1, 5.0), "b" -> (2, 4.0), "c" -> (3, 8.0))
map1: scala.collection.immutable.Map[String,(Int, Double)] = Map(a -> (1,5.0), b -> (2,4.0), c -> (3,8.0))

scala> val map2 = Map("b" -> (4, 1.0))
map2: scala.collection.immutable.Map[String,(Int, Double)] = Map(b -> (4,1.0))

scala> val merge = map1.toMap |+| map2.toMap
merge: scala.collection.immutable.Map[String,(Int, Double)] = Map(a -> (1,5.0), b -> (2,5.0), c -> (3,8.0))
scala> val map2 = Map("d" -> (4, 1.0))
map2: scala.collection.immutable.Map[String,(Int, Double)] = Map(d -> (4,1.0))

scala> val merge2 = map1.toMap |+| map2.toMap
merge2: scala.collection.immutable.Map[String,(Int, Double)] = Map(a -> (1,5.0), b -> (2,4.0), c -> (3,8.0), d -> (4,1.0))

This isn't really ideal, though, since the type (Int, Double) can be used to represent lots of different things, and you've just defined a monoid instance that might turn up in places you or your users don't expect. Personally I'd use a case class instead:

case class Count(order: Int, number: Double)

And then define the instance in the Count companion object, either explicitly or via the countMonoid above and an IsoSet[Count, (Int, Double)].



回答2:

I followed Travis Brown and came with a solution built around the case class, to preven a spillover from the new monoid to every (Int, Double)

import scalaz._, Scalaz._, Isomorphism._
import scalaz.Monoid
import scalaz.std.map._, scalaz.syntax.monoid._

case class MonoidFromIsorphism[F, G](iso: F <=> G)(
  implicit val G: Monoid[G]
) extends IsomorphismMonoid[F, G]

case class TrafficCount(order: Int, number: Double)

object TrafficCount {
  implicit val countMonoid: Monoid[(Int, Double)] = scalaz.std.tuple.tuple2Monoid(
    Monoid.instance[Int](math.min(_, _), Int.MaxValue),
    Monoid.instance[Double](_ + _, 0)
  )
  implicit object TrafficCountMonoid extends MonoidFromIsorphism(
    new IsoSet[TrafficCount, (Int, Double)] {
      def to = (TrafficCount.unapply _) andThen (_.get)
      def from = (TrafficCount.apply _).tupled
    }
  )
}

It works as expected:

val map1 = Map("a" -> TrafficCount(1, 5.0), "b" -> TrafficCount(2, 4.0), "c" -> TrafficCount(3, 8.0))
val map2 = Map("b" -> TrafficCount(4, 1.0))
val map3 = Map("d" -> TrafficCount(4, 1.0))

scala> val merge = map1.toMap |+| map2.toMap
merge: scala.collection.immutable.Map[String,TrafficCount] = Map(a -> TrafficCount(1,5.0), b -> TrafficCount(2,5.0), c -> TrafficCount(3,8.0))

scala> val merge2 = map1.toMap |+| map2.toMap
merge2: scala.collection.immutable.Map[String,TrafficCount] = Map(a -> TrafficCount(1,5.0), b -> TrafficCount(2,5.0), c -> TrafficCount(3,8.0))

In fact we can check the zero for the monoid:

scala> mzero[TrafficCount]
res0: TrafficCount = TrafficCount(2147483647,0.0)

And it does not work where it should not:

val map_1 = Map("a" -> (1, 5.0), "b" -> (2, 4.0), "c" -> (3, 8.0))
val map_2 = Map("b" -> (4, 1.0))
val map_3 = Map("d" -> (4, 1.0))

scala> val merge_1 = map_1.toMap |+| map_2.toMap
<console>:38: error: value |+| is not a member of scala.collection.immutable.Map[String,(Int, Double)]