【问题标题】:Implementing a typeclass using type parameters versus abstract types使用类型参数与抽象类型实现类型类
【发布时间】:2021-02-02 21:36:38
【问题描述】:

继续Witness that an abstract type implements a typeclass 我尝试在下面的代码 sn-p 中并排比较这两种方法:

// 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

我的结论(如果我错了请纠正我)是,如果类型类见证存在于公共构造函数中(如下面的ParamaterizedTC),那么编译器总是可以确定B 实现了AddQuotes。然而,如果这个见证被放在类型类伴随对象的构造函数中(比如WithAbstractTC),那么它就不能。这在一定程度上改变了基于类型参数的方法与基于抽象类型的方法的用法。

【问题讨论】:

  • @DmytroMitin - 如前所述。

标签: scala typeclass implicit


【解决方案1】:

不同之处在于:在ParametrizedTC 中,您拥有类的隐含范围,而在WithAbstractTC 中,您没有。但是当你有一个抽象类型时,没有什么能阻止你添加它:

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)
}

不幸的是,不起作用的是

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)
}

因为它会选择最近范围内的隐含:它试图定义的那个。

【讨论】:

  • 如果您按名称隐藏隐式,后者也可以工作:implicit val ev: AddQuotes[B] = { val ev = ???; implicitly[AddQuotes[_B]] }
  • @DmytroMitin 我不知道这个技巧,但看看它为什么有效,谢谢!
  • @AlexeyRomanov - 谢谢 - 你的第一句话就解决了问题的核心。
【解决方案2】:

implicitly[AddQuotes[contIsWithAbstract.B]]拒绝编译与单个/多个构造函数/apply方法或类型参数/类型成员差异无关。您只是到处丢失了类型改进。编译器无法检查您是否丢失了类型优化。您有权向上转换类型以放弃其改进。

如果你恢复类型细化代码编译

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

请注意implicitly 失去了类型改进,shapeless.the 是安全版本。

implicitly 不够具体时 https://typelevel.org/blog/2014/01/18/implicitly_existential.html

如何通过抽象隐式对类型成员类型类使用类级隐式约束,请参阅@AlexeyRomanov的答案。

【讨论】:

  • 这实际上引发了我一段时间以来一直在思考的一个问题 - 例如,implicit def containerIsWithAbstract 中的返回类型类型注释是否必要?或者编译器可以推断出这个吗?从您的回答看来,如果类型注释出现时没有类型改进,那么编译器会认为结果类型是向上转换的,因此为了避免重复类型改进(或通过不完全重复而意外向上转换),您可以删除它们并让编译器推断类型。
  • 另外 - 感谢 shapeless.theimplicitly 上的指针。我刚刚读完 shapeless for type astronauts guide,它提供了很多思考,我正在尝试将这个库引入我的代码中。
  • @Chrisper 好吧,您可以省略隐含的返回类型并依赖于推断的类型(此外,有时它是唯一的选择,因为在 Scala 中 there are 类型无法在源代码中表达)。但最佳实践是指定隐式类型。有时隐式类型的推断并不符合预期(如果您控制隐式类型会更好),有时它们在编译期间推断得比必要的晚(并且您可能会遇到奇怪的编译错误)。
  • @Chrisper 在 Scala 3 中,未指定类型的隐式将被禁止。可以引入类型Aux,然后返回类型可以写得更简洁:WithAbstractTC.Aux[A, B]
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-01-31
  • 1970-01-01
  • 1970-01-01
  • 2013-09-13
相关资源
最近更新 更多