问题
Following on from Witness that an abstract type implements a typeclass I've tried to compare these two approaches side-by-side in the code snippet below:
// We want both ParamaterizedTC and WithAbstractTC (below) to check that
// their B parameter implements AddQuotes
abstract class AddQuotes[A] {
def inQuotes(self: A): String = s"${self.toString}"
}
implicit val intAddQuotes = new AddQuotes[Int] {}
abstract class ParamaterizedTC[A, _B](implicit ev: AddQuotes[_B]) {
type B = _B
def getB(self: A): B
def add1ToB(self: A): String = ev.inQuotes(getB(self)) // TC witness does not need to be at method level
}
abstract class WithAbstractTC[A] private {
// at this point the compiler has not established that type B implements AddQuotes, even if we have created
// this instance via the apply[A, _B] constructor below...
type B
def getB(self: A): B
def add1ToB(self: A)(implicit ev: AddQuotes[B]): String =
ev.inQuotes(getB(self)) // ... so here the typeclass witness has to occur on the method level
}
object WithAbstractTC {
// This constructor checks that B implements AddQuotes
def apply[A, _B: AddQuotes](getB: A => _B): WithAbstractTC[A] = new WithAbstractTC[A] {
type B = _B
def getB(self: A): B = getB(self)
}
// But we could also have a constructor that does not check, so the compiler can never be certain that
// for a given instance of WithAbstractTC, type B implements AddQuotes
def otherConstructor[A, _B](getB: A => _B): WithAbstractTC[A] { type B = _B } = new WithAbstractTC[A] {
type B = _B
def getB(self: A): B = getB(self)
}
}
case class Container[B: AddQuotes]( get: B )
// These are both fine
implicit def containerIsParamaterized[B: AddQuotes]: ParamaterizedTC[Container[B], B] =
new ParamaterizedTC[Container[B], B] { def getB(self: Container[B]): B = self.get }
implicit def containerIsWithAbstract[_B: AddQuotes]: WithAbstractTC[Container[_B]] =
WithAbstractTC[Container[_B], _B](self => self.get)
val contIsParamaterized: ParamaterizedTC[Container[Int], Int] =
implicitly[ParamaterizedTC[Container[Int], Int]]
val contIsWithAbstract: WithAbstractTC[Container[Int]] =
implicitly[WithAbstractTC[Container[Int]]]
implicitly[AddQuotes[contIsParamaterized.B]]
implicitly[AddQuotes[contIsWithAbstract.B]] // This is not fine
My conclusion (please correct me if I'm wrong) is that if the typeclass witness exists in the public constructor (as in ParamaterizedTC
below) then the compiler can always be certain that B
implements AddQuotes
. Whereas if this witness is put in a constructor in the typeclass companion object (like for WithAbstractTC
) then it cannot. This somewhat changes the usage of a type-parameter-based approach versus the abstract-type-based approach.
回答1:
implicitly[AddQuotes[contIsWithAbstract.B]]
refusing to compile is not connected with single/multiple constructors/apply
methods or type parameter/type member difference. You just lost type refinements everywhere. Compiler can't check that you lost type refinements. You have the right to upcast a type discarding its refinement.
If you restore type refinements the code compiles
object WithAbstractTC {
def apply[A, _B: AddQuotes](getB: A => _B): WithAbstractTC[A] {type B = _B} =
// ^^^^^^^^^^^^^
new WithAbstractTC[A] {
type B = _B
def getB(self: A): B = getB(self)
}
...
}
implicit def containerIsWithAbstract[_B: AddQuotes]:
WithAbstractTC[Container[_B]] { type B = _B } =
// ^^^^^^^^^^^^^^^
WithAbstractTC[Container[_B], _B](self => self.get)
val contIsWithAbstract: WithAbstractTC[Container[Int]] { type B = Int } =
// ^^^^^^^^^^^^^^^^
shapeless.the[WithAbstractTC[Container[Int]]]
//^^^^^^^^^^^^^
implicitly[AddQuotes[contIsWithAbstract.B]] // compiles
Please notice that implicitly
looses type refinements, shapeless.the
is safe version.
When implicitly
isn't specific enough https://typelevel.org/blog/2014/01/18/implicitly_existential.html
How to use class-level implicit constraint for type-member type class via abstract implicit see @AlexeyRomanov's answer.
回答2:
The difference is rather: in ParametrizedTC
you have the implicit in scope of the class, in WithAbstractTC
you don't. But nothing stops you from adding it when you have an abstract type:
abstract class WithAbstractTC2[A] private {
type B
implicit val ev: AddQuotes[B]
def getB(self: A): B
def add1ToB(self: A): String =
ev.inQuotes(getB(self))
}
def apply[A, _B](getB: A => _B)(implicit _ev: AddQuotes[_B]): WithAbstractTC2[A] = new WithAbstractTC2[A] {
type B = _B
implicit val ev: AddQuotes[B] = _ev
def getB(self: A): B = getB(self)
}
What unfortunately won't work is something like
def apply[A, _B: AddQuotes](getB: A => _B): WithAbstractTC2[A] = new WithAbstractTC2[A] {
type B = _B
implicit val ev: AddQuotes[B] = implicitly[AddQuotes[_B]]
def getB(self: A): B = getB(self)
}
because it'll pick the implicit in closest scope: the one it's trying to define.
来源:https://stackoverflow.com/questions/64438704/implementing-a-typeclass-using-type-parameters-versus-abstract-types