使用类型参数和抽象类型实现类型类的方法

7 浏览
0 Comments

使用类型参数和抽象类型实现类型类的方法

在接下来的问题中Witness that an abstract type implements a typeclass之后,

我尝试在下面的代码片段中将这两种方法进行并排比较:

// 我们希望 ParamaterizedTC 和 WithAbstractTC(下面)都能检查它们的 B 参数是否实现了 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 { 
  // 在这个点上,即便我们通过下面的 apply[A, _B] 构造函数创建了这个实例,
  // 编译器也无法确认类型 B 是否实现了 AddQuotes...
  type B 
  def getB(self: A): B
  def add1ToB(self: A)(implicit ev: AddQuotes[B]): String = 
    ev.inQuotes(getB(self)) // ...所以在这里,类型类的 witness 必须出现在方法级别
}
object WithAbstractTC {
  // 这个构造函数检查 B 是否实现了 AddQuotes
  def apply[A, _B: AddQuotes](getB: A => _B): WithAbstractTC[A] = new WithAbstractTC[A] { 
    type B = _B 
    def getB(self: A): B = getB(self)
  }
  // 但我们也可以有一个不检查的构造函数,这样编译器就无法确认对于 WithAbstractTC 的给定实例,
  // 类型 B 是否实现了 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 )
// 这两种方法都可以
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]] // 这是不可以的

我的结论(如果我错了,请纠正我)是,如果类型类的 witness 存在于公共构造函数(如下方的 `ParamaterizedTC`)中,那么编译器总是可以确定 `B` 实现了 `AddQuotes`。而如果这个 witness 放在类型类伴生对象的构造函数中(例如 `WithAbstractTC`),那么它是无法确定的。这在使用基于类型参数的方法和基于抽象类型的方法时会有所不同。

0
0 Comments

在上述内容中,我们可以看到一种使用类型参数实现类型类的方式(ParametrizedTC)和一种使用抽象类型的方式(WithAbstractTC)。两种方式的区别在于,ParametrizedTC中的implicit参数是在类的范围内隐式可用的,而WithAbstractTC中的implicit参数并不是隐式可用的。但是,当使用抽象类型时,我们可以在WithAbstractTC中添加implicit参数。

在代码中,我们可以看到WithAbstractTC2是一个抽象类,其中包含了一个抽象类型B和一个implicit参数ev。该类还定义了一个方法getB和一个方法add1ToB,其中add1ToB方法调用了ev.inQuotes方法对getB方法返回的值进行处理。apply方法是WithAbstractTC2的一个辅助构造方法,接受一个类型为A到类型为_B的函数getB和一个AddQuotes[_B]类型的implicit参数_ev。在apply方法中,我们创建了一个WithAbstractTC2的实例,并将B类型指定为_B,将ev参数赋值为_ev,并实现了getB方法。

然而,对于类似于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参数:正在定义的implicit参数。如果我们隐藏implicit参数的名称,就可以让它正常工作:implicit val ev: AddQuotes[B] = { val ev = ???; implicitly[AddQuotes[_B]] }。

通过上述内容,我们可以看出,使用类型参数和抽象类型的方式实现类型类的主要区别在于implicit参数的作用范围。对于抽象类型的方式,需要在类的定义中添加implicit参数。而对于类型参数的方式,则不需要在类的定义中添加implicit参数,因为它默认在类的范围内隐式可用。这些区别导致了不同的实现方式和使用方法。

0
0 Comments

实现类型类(typeclass)时使用类型参数与抽象类型的区别

在上述代码中,通过implicitly[AddQuotes[contIsWithAbstract.B]]会导致编译错误,这与单个/多个构造函数、apply方法或类型参数/类型成员的区别无关。问题出在你丢失了类型细化(type refinements)。编译器无法检查到你丢失了类型细化。你有权将类型向上转型并丢弃其细化。

如果恢复类型细化,代码将能够编译通过。

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]] // 编译通过

请注意,implicitly丢失了类型细化,而shapeless.the是安全的版本。

implicitly不够具体时,可以参考https://typelevel.org/blog/2014/01/18/implicitly_existential.html

如何通过抽象隐式类级约束使用类型成员类型类,请参考的回答。

这实际上引发了我一直以来心中的一个问题 - 在implicit def containerIsWithAbstract中,返回类型的类型注解是必需的吗?或者编译器能否推断出这个类型?从你的回答来看,如果类型注解没有类型细化,那么编译器会认为结果类型是向上转型的,所以为了避免重复类型细化(或者通过不完整地重复它们而意外地向上转型),你可以删除它们,让编译器推断类型。

此外,感谢你提到了shapeless.theimplicitly的区别。我刚刚完成了阅读《shapeless for type astronauts》指南,其中提供了很多值得思考的内容,并且我正在尝试将该库引入我的代码中。

嗯,你可以省略隐式的返回类型,让其依赖于类型推断(此外,有时这是唯一的选择,因为在Scala中无法在源代码中表示的类型)。但最佳实践是指定隐式的类型。有时隐式的类型可能不如预期地被推断(最好控制隐式的类型),有时它们被推断得比编译过程中的需要晚(可能导致奇怪的编译错误)。

在Scala 3中,没有指定类型的隐式将被禁止。你可以引入类型Aux,然后返回类型可以更紧凑地写为:WithAbstractTC.Aux[A, B]

0