F 有界多态性相对于类型类的优点对于返回电流类型问题

Mar*_*lic 9 scala typeclass f-bounded-polymorphism return-current-type

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

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)
Run Code Online (Sandbox Code Playgroud)

类型成员的 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)
Run Code Online (Sandbox Code Playgroud)

类型类

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)

Run Code Online (Sandbox Code Playgroud)

Lui*_*rez 7

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

更安全是什么意思?简单地说,我们不能破坏返回完全相同类型的约定。对于两种形式的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")
Run Code Online (Sandbox Code Playgroud)

类型参数的 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)
Run Code Online (Sandbox Code Playgroud)

尽管如此,很明显类型类方法更复杂并且有更多的样板文件;另外,有人可能会说,要打破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)
Run Code Online (Sandbox Code Playgroud)