Reputation: 1586
I need an implicit class with a method that lets me merge any immutable map types (<: Map
) that may have duplicate keys & polymorphic values. I can't figure out to get the implicit class to use nested polymorphic types and work implicitly (something like A <: Map[_, B], B <: Combinable[B]
).
I can get it to work on all Map types... or on polymorphic values... but not both. I can't figure out how to combine into one implicit class without the error that it can't find the method in the implicit class.
So for example...
trait Combinable[A] {
this: A =>
def combine(that: A): A
def combine(that: Option[A]): A = that match {
case Some(a) => this combine a
case None => this
}
}
So let's say I have a class...
case class Meta(???) extends Combinable[Meta] {
def combine(that: Meta): Meta = ???
}
Now if I have a standard immutable Map
, it's a cinch... works great.
implicit class CombinableMaps[A <: Combinable[A]](val m1: Map[String, A]) {
def mergeMaps(m2: Map[String, A]): Map[String, A] = {
m1 ++
m2.map { case (k,v) => k -> (v combine m1.get(k)) }
}.asInstanceOf[Map[String, A]]
}
But what if I want it to also work on TreeMap
s and SortedMap
s and whatever else?
implicit class CombinableMaps[B <: Combinable[B], A <: Map[String,B]](val m1: A) {
def mergeMaps(m2: A): A = {
m1 ++
m2.map { case (k,v) => k -> (v combine m1.get(k)) }
}.asInstanceOf[A]
}
This compiles without an error, but when I try using the mergeMap
method it throws error: value mergeMaps is not a member of Map[String,Meta]
.
I tried a variation where B
was a type passed to A
like A[B]
... again, compiled (if I imported scala.language.higherKinds
) but didn't get applied.
Is this kind of nested polymorphism allowed? I can't even figure out what term to search for.
Thanks in advance.
Upvotes: 0
Views: 204
Reputation: 51658
Besides @francoisr's proposal where your approach with F-bounded polymorphism (B <: Combinable[B]
) is proposed to be replaced with ad hoc polymorphism (Combinable
becomes a type class), you can also try to replace bounds with implicit constraints. Try to replace
implicit class CombinableMaps[B <: Combinable[B], A <: Map[String, B]](val m1: A) {
def mergeMaps(m2: A): A = {
m1 ++
m2.map { case (k,v) => k -> (v combine m1.get(k)) }
}.asInstanceOf[A]
}
with
implicit class CombinableMaps[A, B](val m1: A)(implicit
ev: A <:< Map[String, B],
ev1: B <:< Combinable[B]
) {
def mergeMaps(m2: A): A = {
m1 ++
m2.map { case (k,v) => k -> (v combine m1.get(k)) }
}.asInstanceOf[A]
}
or just
implicit class CombinableMaps[A, B <: Combinable[B]](val m1: A)(implicit
ev: A <:< Map[String,B]
) {
def mergeMaps(m2: A): A = {
m1 ++
m2.map { case (k,v) => k -> (v combine m1.get(k)) }
}.asInstanceOf[A]
}
Then Map("a" -> Meta(1), "b" -> Meta(2)).mergeMaps(Map("c" -> Meta(3), "d" -> Meta(4)))
compiles [scastie]
Also I propose to remove this ugly asInstanceOf[A]
[scastie]
import scala.collection.immutable.MapOps
implicit class CombinableMaps[A, B <: Combinable[B],
CC[K,+V] <: MapOps[K, V, CC, _]](val m1: A)(implicit
ev0: A => CC[String, B],
ev2: CC[String, B] => A,
) {
def mergeMaps(m2: A): A = {
m1 ++
m2.map { case (k,v) => k -> (v combine m1.get(k)) }
}
}
or just
implicit class CombinableMaps[B <: Combinable[B],
CC[K,+V] <: MapOps[K, V, CC, _]
](val m1: CC[String, B]){
def mergeMaps(m2: CC[String, B]): CC[String, B] = {
m1 ++
m2.map { case (k,v) => k -> (v combine m1.get(k)) }
}
}
Upvotes: 3
Reputation: 4585
The key to solving your issue is to not try to infer A
immediately. Even something as simple will trip the type inference:
class Foo[B, A <: Map[String, B]](val a: A)
new Foo(Map("foo" -> 42))
inferred type arguments [Nothing,scala.collection.immutable.Map[String,Int]] do not conform to value 's type parameter bounds [B,A <: Map[String,B]]
type mismatch;
found : scala.collection.immutable.Map[String,Int]
required: A
This is because type inference works in "layers": it resolves both A
and B
before it can inspect the "insides" of A
. The solution is to define A differently:
class Foo[B, A[X] <: Map[String, X]](val a: A[B])
new Foo(Map("foo" -> 42))
Let's look at your problem now. Your Combinable
trait would typically be replaced by a type class so that you don't have to extend Combinable
on every type A
you would need to combine. You can just implicitly provide the Combinable
implicitly for every type you need it. This is entirely optional, you could choose to stick with your trait if you prefer that.
trait Combinable[A] {
def combine(first: A, second: A): A
def combine(first: A, second: Option[A]): A = second match {
case Some(a) => combine(first, a)
case None => first
}
}
case class Meta(x: Int)
object Meta {
implicit val combinableInstance: Combinable[Meta] = (first, second) => Meta(first.x + second.x)
}
implicit class CombinableMaps[B : Combinable, A[X] <: Map[String,X]](val m1: A[B]) {
def mergeMaps(m2: A[B]): A[B] = {
m1 ++
m2.map { case (k,v) => k -> implicitly[Combinable[B]].combine(v,m1.get(k)) }
}.asInstanceOf[A[B]]
}
import collection.immutable.TreeMap
val m = TreeMap("foo" -> Meta(42), "bar" -> Meta(43))
val m2 = TreeMap("bar" -> Meta(43))
val m3: TreeMap[String,Meta] = m.mergeMaps(m2)
Upvotes: 4