Dealing with implicit typeclass conflict

牧云@^-^@ 提交于 2021-02-08 08:58:20

问题


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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!