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.