9

在 StackOverflow 上经常会问到返回当前类型的问题。是一个这样的例子。通常的答案似乎是F 有界多态性类型类模式解决方案。Odersky 在Is F-bound polymorphism 有用吗?

F-bounds 确实增加了显着的复杂性。我希望能够摆脱它们,并用更高级的子类型替换它们

而 tpolecat (链接帖子的作者)建议

一个更好的策略是使用类型类,它可以巧妙地解决问题并且几乎没有担心的余地。事实上,在这些情况下完全放弃亚型多态性是值得考虑的。

确定了以下缺点

F-bounded polymorphism 将一个类型参数化为它自己的子类型,这是一个比用户通常想要的更弱的约束,这是一种说“我的类型”的方式,你不能通过子类型精确地表达它。然而类型类可以直接表达这个想法,所以这就是我要教初学者的

我的问题是,根据上述建议,有人可以证明 F 有界多态性是有利的情况,还是我们应该将类型类解决方案作为解决返回电流类型问题的规范答案?

类型参数的 F 绑定多态性

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 有界多态性

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)

4

3 回答 3

4

F-Bounded是类型系统能够表达的一个很好的例子,甚至是更简单的类型系统,比如 Java 的。但是,类型类总是更安全、更好的选择。

更安全是什么意思?简单地说,我们不能违反返回完全相同类型的合同。这可以针对两种形式的F 有界多态性(很容易)完成。

类型成员的 F 有界多态性

这个很容易破解,因为我们只需要对 type member 撒谎

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 有界多态性

这个有点难破解,因为它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)

尽管如此,很明显typeclass方法更复杂并且有更多样板。此外,有人可能会争辩说,要打破F-Bounded,你必须故意这样做。因此,如果您对F-Bounded的问题感到满意并且不喜欢处理类型类的复杂性,那么它仍然是一个有效的解决方案。

此外,我们应该注意,即使是typeclassasInstanceOf方法也可以通过使用或反射之类的东西来破坏。


顺便说一句,值得一提的是,如果您不想返回修改后的副本,而是想要修改当前对象并返回自身以允许链接调用(如传统的 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)
于 2020-01-20T16:24:06.457 回答
1

我会建议类型类确实是优越的模式,对于“返回当前类型”问题的任何 F 绑定多态解决方案都具有同样好的,如果不是更好的类型类并行。

F-bound 多态方法实际上并不能很好地表达“当前类型”的概念,而类型类可以。在组合优于继承的原则下,类型类还可以生成通常好的代码。这个答案提供了类似的逻辑,特别是参考了 scala 类型类。

注意:我不是权威;似乎这可能是正确的答案(如问题中所暗示的那样),并且需要被表示。

于 2020-01-19T20:18:26.690 回答
0

根据我的经验

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

2 你确实需要类型类。

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

考虑 3 个我碰巧熟悉的例子。

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

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

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

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

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

因此,我们希望能够对 Shape 执行剪切操作。我们希望剪切操作给我们一个形状。我们不想密封我们的 Shape trait,所以我们需要使用继承来实现它。当然,如果想要剪切 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 绑定类型参数。一个死了,一个疯了,另一个忘记了。

将继承对象和类型类实例中的方法数量保持在最低限度。将所有可以背负其他实例实现方法的辅助方法放入类型类的扩展类中。

倾向于避免非 final 类,如果你在其伴生对象中使用带有 apply 和 unapply 方法的 trait,你将获得类的许多优点,同时避免非 final 类的问题。

于 2021-01-21T12:40:54.073 回答