F-bounded多态性在解决返回当前类型问题方面的优势相比于typeclass

11

返回当前类型的问题在StackOverflow上经常被问到。这里是一个例子。通常的答案似乎要么是F-bounded polymorphism,要么是typeclass模式解决方案。Odersky在F-bound polymorphism useful吗?中建议:

F-bounds确实增加了相当大的复杂性。我很想能够摆脱它们,并用高级子类型替换它们。

而tpolecat(链接post的作者)则建议:

更好的策略是使用类型类,它可以干净利落地解决问题,几乎没有什么可担心的余地。事实上,在这些情况下考虑完全放弃子类型多态性是值得的。

其中确定了以下disadvantage

F-bounded多态性是将类型参数化为其自身的子类型,这是用户通常想要的比较弱的约束条件,用户通常希望以一种方式表达“我的类型”,这不能通过子类型精确地表达。然而,类型类可以直接表达这个想法,所以这就是我会教初学者的东西。
我的问题是,在上述建议的基础上,是否有人能演示出F-bounded多态性有利的情况,或者我们应该将类型类解决方案作为解决“返回当前类型”问题的规范答案?
F-bound polymorphism by type parameter.
trait Semigroup[A <: Semigroup[A]] { this: A =>
  def combine(that: A): A
}

final case class Foo(v: Int) extends Semigroup[Foo] {
  override def combine(that: Foo): Foo = Foo(this.v + that.v)
}

final case class Bar(v: String) extends Semigroup[Bar] {
  override def combine(that: Bar): Bar = Bar(this.v concat that.v)
}

def reduce[A <: Semigroup[A]](as: List[A]): A = as.reduce(_ combine _)

reduce(List(Foo(1), Foo(41)))        // res0: Foo = Foo(42)
reduce(List(Bar("Sca"), Bar("la")))  // res1: Bar = Bar(Scala)

通过类型成员实现F-bounded多态

trait Semigroup {
  type A <: Semigroup
  def combine(that: A): A
}

final case class Foo(v: Int) extends Semigroup {
  override type A = Foo
  override def combine(that: Foo): Foo = Foo(this.v + that.v)
}

final case class Bar(v: String) extends Semigroup {
  override type A = Bar
  override def combine(that: Bar): Bar = Bar(this.v concat that.v)
}

def reduce[B <: Semigroup { type A = B }](as: List[B]) =
  as.reduce(_ combine _)

reduce(List(Foo(1), Foo(41)))        // res0: Foo = Foo(42)
reduce(List(Bar("Sca"), Bar("la")))  // res1: Bar = Bar(Scala)

类型类

trait Semigroup[A] {
  def combine(x: A, y: A): A
}

final case class Foo(v: Int)
object Foo {
  implicit final val FooSemigroup: Semigroup[Foo] = 
    new Semigroup[Foo] {
      override def combine(x: Foo, y: Foo): Foo = Foo(x.v + y.v)
    }
}

final case class Bar(v: String)
object Bar {
  implicit final val BarSemigroup: Semigroup[Bar] = 
    new Semigroup[Bar] {
      override def combine(x: Bar, y: Bar): Bar = Bar(x.v concat y.v)
    }
}

def reduce[A](as: List[A])(implicit ev: Semigroup[A]): A = as.reduce(ev.combine)

reduce(List(Foo(1), Foo(41)))        // res0: Foo = Foo(42)
reduce(List(Bar("Sca"), Bar("la")))  // res1: Bar = Bar(Scala)


是的,就像那样。然而,Monoid是类型类完全必要的一个非常好的例子。 - Luis Miguel Mejía Suárez
@LuisMiguelMejíaSuárez 哦,我明白了,因为“unit”。我会将其更改为semigroup。 - Mario Galic
@DmytroMitin 只使用 type A <: Monoid 会有什么问题? - Mario Galic
@MarioGalic的trait Monoid { type A <: Monoid }trait Monoid[A <: Monoid[_]]类似。 - Dmytro Mitin
@MarioGalic 如果不进行类型细化,Monoid 就是一种存在类型。 - Dmytro Mitin
显示剩余5条评论
4个回答

7
F-Bounded是一个很好的例子,展示了类型系统可以表达的能力,即使像Java这样的简单语言也可以。但是,typeclass总是更安全和更好的选择。

什么是“更安全”?简单来说,我们不能打破返回完全相同类型的协议。而这可以通过两种形式的F-Bounded多态性(非常容易)实现。

通过类型成员的F-bounded多态性

这种方法很容易被破坏,因为我们只需要在类型成员上撒谎就行了

trait Pet {
  type P <: Pet
  def name: String 
  def renamed(newName: String): P
}

final case class Dog(name: String) extends Pet {
  override type P = Dog
  override def renamed(newName: String): Dog = Dog(newName)
}

final case class Cat(name: String) extends Pet {
  override type P = Dog // Here we break it.
  override def renamed(newName: String): Dog = Dog(newName)
}

Cat("Luis").renamed(newName = "Mario")
// res: Dog = Dog("Mario")

通过类型参数实现F-bounded多态性

这个可能有些难以理解,因为this: A规定了扩展类必须相同。不过,我们只需要增加一个继承层次即可。

trait Pet[P <: Pet[P]] { this: P =>
  def name: String 
  def renamed(newName: String): P
}

class Dog(override val name: String) extends Pet[Dog] {
  override def renamed(newName: String): Dog = new Dog(newName)

  override def toString: String = s"Dog(${name})"
}

class Cat(name: String) extends Dog(name) // Here we break it.

new Cat("Luis").renamed(newName = "Mario")
// res: Dog = Dog(Mario)

尽管如此,显然类型类方法更为复杂,需要更多的样板文件;同时,人们可以认为要打破F-Bounded,必须有意识地这样做。因此,如果你能接受F-Bounded的问题,并且不想处理类型类的复杂性,那么它仍然是一个有效的解决方案。
此外,我们应该注意到,即使使用类型类方法,也可以通过使用asInstanceOf或反射等方式来打破它。
顺便提一下,值得一提的是,如果您不是返回修改后的副本,而是要修改当前对象并返回自身以允许调用链接(像传统的Java构建器一样),则可以(应该)使用this.type
trait Pet {
  def name: String

  def renamed(newName: String): this.type
}

final class Dog(private var _name: String) extends Pet {
  override def name: String = _name

  override def renamed(newName: String): this.type = {
    this._name = newName
    this
  }

  override def toString: String = s"Dog(${name})"
}

val d1 = Dog("Luis")
// d1: Dog = Dog(Luis)

val d2 = d1.renamed(newName = "Mario")
// d2: Dog = Dog(Mario)

d1 eq d2
// true

d1
// d1: Dog = Dog(Mario)

1
这个类型是一个很棒的注释,我之前一直在摸索它。 - Tim

1
我建议类型类确实是更优秀的模式,对于“返回当前类型”的任何F-bound多态解决方案都有同样好甚至更好的类型类并行。F-bound多态方法实际上并没有很好地表达“当前类型”概念,而类型类可以。根据组合优于继承原则,类型类也可以产生通常良好的代码。这个答案提供了类似的逻辑,特别是参考Scala类型类。注意:我不是一个权威人士;只是似乎这可能是正确的答案(如问题中所示),需要被代表。

0

从我的经验来看:

1 不要使用 F-Bound 类型参数。

2 你确实需要类型类。

3 直接继承仍然是类型类的一个非常有用的补充。

考虑我熟悉的三个示例。

Show[T]:从对象 / 数据值生成字符串表示。

Unshow[T]:从字符串表示生成对象 / 数据值。

Shear[T]:对对象执行剪切几何变换。

所以 Unshow[T] 很直接,我们必须使用类型类和仅限于类型类。在生成它之前,我们没有类型为 T 的对象可用于操作。直接继承是不可能的。我说直接继承,因为我们仍然可能希望在类型类实现中使用继承。我们可能还希望在类型类层次结构中使用继承,但让我们忽略这种复杂性,并从现在开始假设这不是我所指的继承。但请注意,对于 Unshow[T] 的代数和类型实现非常直接。Unshow[T],其中 T 是代数和类型,本质上是一种查找操作,您遍历 T 的所有子类型列表,直到找到成功返回子类型值的子类型为止。例如,对于 UnShow[Option[A]],您尝试 Unshow[Some[A]],如果失败,则尝试 Unshow[None]。虽然实现简单,但最大的限制是在创建 Unshow[T] 实例时,您必须知道 T 的所有子类型。

对于Show[T]而言,它只是一个被美化或重新构想的toString方法,我们实际上不需要缩小方法或方法的返回类型。但我想谈谈它,因为它在概念上很简单,并且说明了继承的优点。如果我们想要一个Show[List[A]]、Show[Array[A]]或Show[Option[A]],我们将不得不使用类型类来使用Show[T]。然而,继承对于代数和类型的实现仍然非常有用。如果Type[T]的Show[T]类型类实例委托给一个继承方法,那么即使在实现时并不知道T的所有子类型,也可以实现类型T的实例。即使在代数和类型的所有子类型在实例创建时都已知的情况下,即使类型T实际上是封闭的,使用继承方法委托也比使用匹配语句更加清晰。

因此,我们希望能够在形状上执行剪切操作,并希望剪切操作将形状返回给我们。我们不想封闭我们的形状特征,因此我们需要使用继承来实现这一点。当然,如果我们想要剪切List [shape]、Array [Shape]或Option [Shape],我们仍然需要一个类型类。因此,为简单起见,让我们假设我们有:

trait Shape {
  def xShear(operand: Double): Shape
}

那么我们可以保持抽象的同时简化返回类型

trait Polygon extends Shape {
  override def xShear(operand: Double): Polygon
}

trait Triangle extends Polygon {
  override def xShear(operand: Double): Triangle = { implementation }
}

object Triangle {
  /** This allows us to create Triangles as if it was a class while keeping it a trait. */
  def apply(stuff): Trangle = { blah blah}
  def unapply(inp: Any): [(Pt, Pt, Pt)] = { blah, blah }
}

class EquilateralTriangle(stuff) extends Triangle {
  //Doesn't override xShear as can not fulfil the interface
}

最终建议。

如果您有多个方法返回类型想要保持同步,则使用类型成员而不是类型参数。据说有3个人完全学会如何安全使用F bound类型参数,其中一个死了,一个疯了,另一个忘记了。

将继承对象和类型类实例中的方法数量保持最少。将所有可以依赖其他已实现方法的帮助程序方法放入类型类的扩展类中。

尽量避免使用非最终类,如果在其伴随对象中使用具有apply和unapply方法的特质,则可以获得大部分类的优点,同时避免非最终类的问题。


0

是的,F-bounded polymorphism与返回类型中的“self”成员类型不同。如果您有一个类型层次结构并调用返回“相同类型”的某个抽象类a:A的方法,则在F-bounded polymorphism中结果为A。在绑定类型成员方法中,结果为a.Self

比较:

   trait Base1[+Self <: FBound[Self]] {
      def copy :Self
   }
   trait Sub1 extends Base1[Sub1]
   class Impl1 extends Sub1 with Base1[Impl1] {
      def copy = this
   }

   trait Base2 {
      type Self <: Base
      copy :Self 
   }
   trait Sub2 extends Base2 { type Self <: Sub2 }
   class Impl2 extends Sub2 {
      type Self = Impl2
      def copy = this
   }

   def copy1[T <: Base1[T]](x :T) = x.self
   def copy2(x :Base2) = x.self
   
   val proto1 = new Impl1 :Sub1
   val proto2 = new Impl2 :Sub2
   val a = copy1(proto1)
   val b = copy2(proto2)

a 的类型是 Sub1,但 b 的类型是 proto2.Self <: Sub2。 这可以成为成员类型的优点:方法 copy2 更简单,并且无论您传递多么具体的对象,您都将获得完全相同类型的值。

然而,递归应用时却是个巨大的痛点:

   class Ev1[T <: Base1[T]] 
   implicit val sub1Ev = new Ev1

   class Ev2[T <: Base2] 
   val sub2Ev = new Ev2

   class Foo
   implicit def foo1[T <: Base1[T]](x :T)(implicit ev :Ev1[T]) = new Foo
   implicit def foo2[T <: Base2](x :T)(implicit ev :Ev2[T]) = new Foo

   foo1(proto1.self) //compiles
   foo2(proto2.self) //does not compile
   def foo[T](x :T)(implicit ev:T => Foo) = ev(x)
  
   foo1(proto1.self) //compiles
   foo2(proto2.self) //does not compile

   foo(proto1.self) //ok
   foo(proto2.self)  //implicit not found

隐式类型参数是一种痛苦,因为现在总是有可能手动指定类型参数。根据实际情况的复杂程度,通常可以通过一些技巧来引导编译器,但这需要相当多的经验和时间,而且方法签名更加复杂。

类型类在Haskel中非常好用,或者当你处理不相关的类型时也很好用,但是当你处理类型层次结构时最糟糕,因为它们只能操作它们所知道的类型。如果你要对 T <: Sub1/T <: Sub2 进行一些修改,你只有 Sub1 的隐式类型参数,这可能无法返回一个 Impl1。你不能总是将对类型类的需求传播到完整的操作类型已知的类型上,因为这可能会导致方法有数十个隐式参数,其签名会随着实现而改变。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接