【问题标题】:Advantages of F-bounded polymorphism over typeclass for return-current-type problem对于返回电流类型问题,F 有界多态性相对于类型类的优势
【发布时间】:2020-05-05 20:59:15
【问题描述】:

Returning the current type 问题经常在 StackOverflow 上被问到。 Here 就是这样一个例子。通常的答案似乎是F-bounded polymorphismtypeclass 模式解决方案。奥德斯基建议Is F-bound polymorphism useful?

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

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

更好的策略是使用类型类,它可以解决问题 整洁,几乎没有担心的余地。其实很值得 考虑在这些中完全放弃亚型多态性 情况。

以下disadvantage 的标识

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

我的问题是,根据上述建议,有人可以证明 F 有界多态性是有利的情况,还是我们应该将 typeclass 解决方案作为解决 return-current-type 有问题吗?

类型参数的 F-bound 多态性

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)

【问题讨论】:

  • @LuisMiguelMejíaSuárez this 你所说的F-bounded by type member而不是类型参数是什么意思?
  • 是的,完全一样。然而,Monoid 是一个非常好的例子,说明了何时完全需要 typeclass
  • @LuisMiguelMejíaSuárez 啊,我明白了,因为unit。我将其更改为半组。
  • @MarioGalic 实际上对于真正的 F-bound 这应该是 trait Monoid { self =&gt; type A &lt;: Monoid { type A = self.A }... 而不仅仅是 trait Monoid { type A &lt;: Monoid...
  • @DmytroMitin 与type A &lt;: Monoid 有什么关系?

标签: scala typeclass f-bounded-polymorphism return-current-type


【解决方案1】:

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

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

类型成员的 F 有界多态性

这个很容易破解,因为我们只需要对类型成员撒谎

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 polymorphism by type parameter

这个有点难破解,因为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 的问题并且不喜欢处理 typeclass 的复杂性,那么它仍然是一个有效的解决方案。

另外,我们应该注意,即使是 typeclass 方法也可以通过使用 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)

【讨论】:

  • this.type 是一个很棒的笔记,我是在磕磕绊绊的。
【解决方案2】:

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

F-bound 多态方法实际上并不能很好地表达“当前类型”的概念,而类型类可以。在composition is better than inheritance 的原则下,类型类还可以编写出通常很好的代码。 This answer 提供了类似的逻辑,特别是针对 scala 类型类。

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

【讨论】:

    【解决方案3】:

    根据我的经验

    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]。虽然实现起来很简单,但最大的限制是在创建 Unshow[T] 实例时必须知道 T 的所有子类型。

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

    所以我们希望能够对 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 类的问题。

    【讨论】:

      猜你喜欢
      • 2011-10-20
      • 2015-03-09
      • 1970-01-01
      • 2016-09-22
      • 2021-12-29
      • 2010-10-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多