Implementing a typeclass using type parameters versus abstract types

假如想象 提交于 2021-01-29 10:51:22

问题


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

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