问题
I'm trying to deal with an ambiguous implicits problem, and (relatedly) figure out what best practise should be for parameterizing typeclasses. I have a situation where I am using a typeclass to implement a polymorphic method. I initially tried the approach below:
abstract class IsValidTypeForContainer[A]
object IsValidTypeForContainer {
implicit val IntIsValid = new IsValidTypeForContainer[Int] {}
implicit val DoubleIsValid = new IsValidTypeForContainer[Double] {}
}
abstract class IsContainer[A, B: IsValidTypeForContainer] {
def getElement(self: A, i: Int): B
def get[R](self: A, ref: R)(implicit gets: GetsFromContainerMax[A, R, B]): gets.Out = gets.get(self, ref)
def fromList(self: A, other: List[B]): A = ???
}
implicit class IsContainerOps[A, B: IsValidTypeForContainer](self: A)(implicit
isCont: IsContainer[A, B],
) {
def getElement(i: Int) = isCont.getElement(self, i)
def get[R](ref: R)(implicit gets: GetsFromContainerMax[A, R, B]): gets.Out = isCont.get(self, ref)
def fromList(other: List[B]): A = isCont.fromList(self, other)
}
abstract class GetsFromContainerMax[A, R, B: IsValidTypeForContainer] {
type Out
def get(self: A, ref: R): Out
}
object GetsFromContainerMax {
type Aux[A, R, B, O] = GetsFromContainerMax[A, R, B] { type Out = O }
def instance[A, R, B: IsValidTypeForContainer, O](func: (A, R) => O): Aux[A, R, B, O] = new GetsFromContainerMax[A, R, B] {
type Out = O
def get(self: A, ref: R): Out = func(self, ref)
}
implicit def getsForListInt[A, B: IsValidTypeForContainer](implicit
isCons: IsContainer[A, B],
): Aux[A, List[Int], B, A] = instance(
(self: A, ref: List[Int]) => {
val lst = ref.map(isCons.getElement(self, _)).toList
isCons.fromList(self, lst)
}
)
}
Where I have given the GetsContainerMax
typeclass three parameters - one for the IsContainer
object, one for the reference and one for the data type of the IsContainer
object.
When I then try to use this, I get a compile error:
case class List1[B: IsValidTypeForContainer] (
data: List[B]
)
implicit def list1IsContainer[B: IsValidTypeForContainer] = new IsContainer[List1[B], B] {
def getElement(self: List1[B], i: Int): B = self.data(i)
def fromList(self: List1[B], other: List[B]): List1[B] = ???
}
val l1 = List1[Int](List(1,2,3))
implicitly[IsContainer[List1[Int], Int]].get(l1, List(1,2)) // Works
implicitly[List1[Int] => IsContainerOps[List1[Int], Int]] // Works
l1.get(List(1,2)) // Does not work
If I use the -Xlog-implicits build parameter, it tells me that
ambiguous implicit values: both value IntIsValid in object IsValidTypeForContainer of type example.Test.IsValidTypeForContainer[Int] and value DoubleIsValid in object IsValidTypeForContainer of type example.Test.IsValidTypeForContainer[Double] match expected type example.Test.IsValidTypeForContainer[B]
Which seems to make sense; presumably I am bringing both of these implicits into scope by parameterizing the typeclass with the generic B
.
My next thought was therefore to try to reduce the number of generic parameters for IsValidTypeForContainer
to the minimum, in order to have only one typeclass in scope per type of R
, likeso:
abstract class GetsFromContainerMin[R] {
type Out
def get[A](self: A, ref: R)(implicit isCont: IsContainer[A, _]): Out
}
object GetsFromContainerMin {
type Aux[R, O] = GetsFromContainerMin[R] { type Out = O }
def instance[A, R, O](func: (A, R) => O): Aux[R, O] = new GetsFromContainerMin[R] {
type Out = O
def get(self: A, ref: R)(implicit isCont: IsContainer[A, _]): Out = func(self, ref)
}
implicit def getsForListInt[A](implicit isCons: IsContainer[A, _]): Aux[List[Int], A] = instance(
(self: A, ref: List[Int]) => {
val lst = ref.map(isCons.getElement(self, _)).toList
isCons.fromList(self, lst) // type mismatch - found: List[Any], required: List[_$3]
}
)
}
But this seems to not only not solve the problem, but to generate an additional error in that the compiler can no longer type-check that type B implements IsValidTypeForContainer
.
Any help gratefully received.
回答1:
So I've messed around with this a bit and seem to have found the solution. The typeclass-with-three-parameters approach works, if I use
implicit class IsContainerOps[A, B](self: A)(implicit
isCont: IsContainer[A, B],
)
instead of
implicit class IsContainerOps[A, B: IsValidTypeForContainer](self: A)(implicit
isCont: IsContainer[A, B],
)
I am not exactly sure why this is and if anyone would care to respond I'd be interested to know. Are multiple typeclasses created if you use a context bound like in the original example, one for each implementation of IsValidTypeForContainer
?
回答2:
Regarding GetsFromContainerMin
, only type classes without polymorphic methods can have constructor method instance
(in companion object) because of the lack of polymorphic functions in Scala 2. In Scala 3 you'll be able to write
def instance[R, O](func: [A] => (A, R) => O): Aux[R, O] = ...
So far you have to write
object GetsFromContainerMin {
type Aux[R, O] = GetsFromContainerMin[R] { type Out = O }
implicit def getsForListInt[A](implicit isCons: IsContainer[A, _]): Aux[List[Int], A] = new GetsFromContainerMin[List[Int]] {
override type Out = A
override def get[A1](self: A1, ref: List[Int])(implicit isCont: IsContainer[A1, _]): Out = {
val lst = ref.map(isCons.getElement(self, _)).toList
// ^^^^
isCons.fromList(self, lst)
// ^^^^
}
}
}
I guess compile errors are pretty clear
type mismatch;
found : self.type (with underlying type A1)
required: A
Regarding your first question,
implicit class IsContainerOps[A, B: IsValidTypeForContainer](self: A)(implicit
isCont: IsContainer[A, B]
)
is desugared to
implicit class IsContainerOps[A, B](self: A)(implicit
ev: IsValidTypeForContainer[B],
isCont: IsContainer[A, B]
)
but order of implicits is significant. If you modify IsContainerOps
to
implicit class IsContainerOps[A, B](self: A)(implicit
isCont: IsContainer[A, B],
ev: IsValidTypeForContainer[B],
)
then l1.get(List(1,2))
will compile.
Implicits are resolved from left to right. You want firstly A
to be inferred (from self
), then IsContainer[A, B]
to be resolved, so B
to be inferred and IsValidTypeForContainer[B]
to be resolved and not vice versa firstly A
to be inferred, then IsValidTypeForContainer[B]
to be resolved, and at this step B
is not restricted, there is no connection between A
and B
, so possibly B
can be not inferred or inferred to be Nothing
, so IsContainer[A, B]
is not resolved. I'm a little simplifying (not every time when you swap implicits you break resolution) but general strategy of implicit resolution and type inference is as I described.
来源:https://stackoverflow.com/questions/64130596/dealing-with-implicit-typeclass-conflict