【问题标题】:Polymorphic updating of immutable classes不可变类的多态更新
【发布时间】:2014-02-12 05:56:27
【问题描述】:

我一直试图解决这个问题,但我似乎无法找到解决这个问题的方法。我似乎无法在 Scala 中正确建模。

假设我有一个 trait MyTrait 和一些不可变的类来实现它。

它看起来像这样:

trait MyTrait {
  type Repr <: MyTrait

  def substitute(original: Item, replacement: Item) : Repr

  def substituteAll(
    originals: List[Item],
    replacement: Item
  ) : Repr = {
    originals match {
      case head :: tail => substitute(head).substituteAll(tail, replacement)
      case Nil => this //this complains that this is not of type Repr
    }     
  }
}

trait MyTrait2 { ... }

case class MyClassA(originals: List[Item])
extends MyTrait with MyTrait2 {
  type Repr = MyClassA

  def substitute(original: Item, replacement: Item) : MyClassA = {
    //whatever code that updates the list etc. 

    MyClassA(newOriginals)
  }    
}

case class MyClassB(originals: List[Item])
extends MyTrait with MyTrait2 {
  type Repr = MyClassB

  def substitute(original: Item, replacement: Item) : MyClassB = {
    //whatever code that updates the list etc. 

    MyClassB(newOriginals)
  }    
}

case class CompoundClass(list : List[MyTrait with MyTrait2])
extends MyTrait {
  type Repr = CompoundClass

  def substitute(
    original: Item,
    replacement: Item
  ) : CompoundClass = 
    CompoundClass(list.map(
      myClass => myClass.substitute(original, replacement)
    ))
  )
  //the above complains that it is expecting
  // List[MyTrait with MyTrait2]
  //while in fact it is getting MyTrait2#Repr
}

如果我要总结我的问题,它们会如下:

  • 通过 super-trait 的方法更新不可变类应该返回与实现类相同的类型。

  • Super-trait 需要能够在有意义的时候返回 this,而不是返回另一个对象。我似乎对此有疑问。

  • 我需要能够在不知道实际类型的情况下将超级特征传递给函数。标准多态性。这样该函数就可以调用 trait 的方法,而无需 了解实际的具体类型。

  • 我需要能够使用组合将类组合成其他类。 (想象一个由子表达式组成的表达式)。这似乎是标准组合,但在上述情况下,#Repr

  • 出现了这些错误

我最初尝试在MyTrait[T] 中使用泛型类型,但这使得无法传递我想要的任何具体类。我现在正在尝试使用抽象类型,我似乎在编译时面临同样的问题。本质上我认为现在真的没有区别,我又陷入了同样的陷阱。

我做错了什么?我看错了吗?

【问题讨论】:

  • 乍一看,我怀疑有些你想要的东西是不可能的。但是请查看“F-Bounded Polymophism”(在 Scala 中)以了解允许您实现第一个请求的模式:“通过超级特征的方法更新不可变类应该返回与实现类相同的类型。”
  • @KevinWright 是的,它是对我之前问题的改进。我的方案仍然有一些困难,比前一个更详细,前一个不起作用。
  • @RandallSchulz 我的第一个解决方案是使用 F-Bounded Polymorphism,这就是我对 MyTrait[T] 的意图。但是,我遇到了一个问题,即无论我期望 MyTrait 作为参数,编译器都开始期待 [T] 的某种类型,结果我一团糟。 KevinWright(在上一个问题中)建议我改用抽象类型成员,这在小示例中似乎效果很好,但实际上我什至无法将 this 返回到从超类返回此类型成员的函数,并将实例作为参数传递开始得到#Repr
  • 正如 Randall 所说,对于这些用例,您将需要带有类型参数的完整 F 绑定。您仍然可以通过在层次结构中放置另一层来隐藏参数,但这不是从我的手机上解释的最简单的事情!如果其他人在此期间没有这样做,我会在早上回答。
  • @KevinWright 谢谢。如果您想回答前面的示例问题,那也可以。我不知道为什么我会遇到抽象类型成员的问题,实际上它似乎也遇到了同样的问题。我期待使用type Repr &lt;: MyTrait 并在子类中覆盖它会起作用,但我却得到了这个奇怪的#Repr 后缀编译错误。

标签: scala inheritance polymorphism abstract-type


【解决方案1】:

这里有一些问题,其中一些与多态性完全无关!

substituteAll 方法开始:

trait MyTrait {
  type Repr <: MyTrait

  def substitute(original: Item, replacement: Item) : Repr

  def substituteAll(
    originals: List[Item],
    replacements: List[Item]
  ) : Repr = {
    originals match {
      case head :: tail => substitute(head).substituteAll(tail)
      case Nil => this
    }     
  }
}

substitutesubstituteAll 都接受两个参数,但您试图用一个参数调用它们。这永远行不通!

您还有一个问题是编译器没有证据表明thisRepr。第一个问题可以很容易地解决,方法是将两个输入压缩到一个元组列表中,然后使用内部函数,第二个问题可以通过手动提供证据来解决:

trait MyTrait {
  type Repr <: MyTrait

  def substitute(original: Item, replacement: Item) : Repr

  def substituteAll(
    originals: List[Item],
    replacements: List[Item]
  )(implicit typed: this.type => Repr) : Repr = {
    def loop(pairs: List[(Item, Item)]): Repr = pairs match {
      case (orig, rep) :: tail =>
        substitute(orig, rep)
        loop(tail)
      case Nil => typed(this) //this complains that this is not of type Repr
    }
    loop(originals zip replacements)
  }
}

您的下一个问题是 Repr 类型参数不包含您希望它为复合类型 MyTrait with MyTrait2 保留的内容。

这不是一个完整的类型,因为Repr 参数仍然是抽象的。你真正想要的是完全指定的类型MyTrait with MyTrait2 { type Repr = MyTrait with MyTrait2 }

鉴于这有点麻烦,引入另一个特征来表示它会更容易:

trait CompoundElem extends MyTrait with MyTrait2 {
  type Repr <: CompoundElem
}

然后您可以在其余代码中使用它:

case class MyClassA(originals: List[Item]) extends CompoundElem {
  type Repr = MyClassA

  def substitute(original: Item, replacement: Item) : MyClassA = {
    val newOriginals = originals
    MyClassA(newOriginals)
  }
}

case class MyClassB(originals: List[Item]) extends CompoundElem {
  type Repr = MyClassB

  def substitute(original: Item, replacement: Item) : MyClassB = {
    val newOriginals = originals
    MyClassB(newOriginals)
  }
}

case class CompoundClass(list : List[CompoundElem]) extends MyTrait {
  type Repr = CompoundClass

  def substitute(
    original: Item,
    replacement: Item
  ) = CompoundClass(
   list.map( _.substitute(original, replacement) )
  )  
}

如果你想让元素类型保持更长时间,也可以编写最后一个类:

object MyTrait {
  //type alias helper to view the type member as though it were a param
  //A neat trick, shamelessly borrowed from the shapeless library
  type Aux[R] = MyTrait { type Repr = R }
}

case class CompoundClass[E <: MyTrait.Aux[E]](list : List[E]) extends MyTrait {
  type Repr = CompoundClass[E]

  def substitute(
    original: Item,
    replacement: Item
  ) = CompoundClass(
    list.map( _.substitute(original, replacement) )
  )  
}

以这种方式编写,您在大多数情况下也不需要中间特征。例如,包含ClassA 列表的CompoundClass 就可以正常工作。

如果你想要一个 ListClassAClassB 的组合,那么你仍然需要中间体来帮助编译器准确解开 Repr 应该是什么,它无法计算ClassA with ClassBRepr 也应该是 ClassA with ClassB

如果您不介意完全擦除元素类型,您可以这样做:

case class CompoundClass(list : List[MyTrait]) extends MyTrait {
  type Repr = CompoundClass     
  def substitute(
    original: Item,
    replacement: Item
  ) = CompoundClass(
    list.map( _.substitute(original, replacement) )
  )
}

它只是MyTraits 中的CompoundClass,当你拉取元素以将它们视为ClassAClassBMyTrait2 时,你需要使用模式匹配,但你赢了'不需要任何中间特征。


最后...作为参考,这是使用 F-Bounds 重新实现的相同想法。请注意类型参数如何允许self type,因此您无需在所有子类中显式定义Repr

trait Item {}

trait MyTrait {
  type Repr <: MyTrait

  def substitute(original: Item, replacement: Item) : Repr  
  def substituteAll(originals: List[Item], replacements: List[Item]) : Repr
}

object MyTrait {
  trait Aux[T <: MyTrait.Aux[T]] extends MyTrait { self: T =>
    type Repr = T

    def substituteAll(originals: List[Item], replacements: List[Item]) : T = {
      def loop(pairs: List[(Item, Item)]): Repr = pairs match {
        case (orig, rep) :: tail =>
          substitute(orig, rep)
          loop(tail)
        case Nil => this
      }
      loop(originals zip replacements)
    }
  }
}

trait MyTrait2 { }

case class MyClassA(originals: List[Item]) extends MyTrait.Aux[MyClassA] with MyTrait2 {
  def substitute(original: Item, replacement: Item) = MyClassA(originals)
}

case class MyClassB(originals: List[Item]) extends MyTrait.Aux[MyClassB] with MyTrait2 {
  def substitute(original: Item, replacement: Item) = MyClassB(originals)
}

case class CompoundClass(list : List[MyTrait]) extends MyTrait.Aux[CompoundClass] {
  def substitute(
    original: Item,
    replacement: Item
  ) = CompoundClass(
    list.map( _.substitute(original, replacement) )
  )
}

【讨论】:

  • 谢谢。 substituteAll 问题只是我将代码抽象为一个更简单的示例以在此处显示时出现的错字,太累了,无法注意到错误。无论如何,你明白我的意思:)。更详细地查看您的答案...
  • 所以,(implicit typed: this.type =&gt; Repr) 工作出色。我不知道我可以在那里指定类型。不过,关于Repr,我仍然不完全在那里。 MyTrait 为什么要关心具体类是否真的实现了其他特征?我不可能满足我可能在不同的具体类中实现的所有可能的特征(我有大约 10 个,它们混合并匹配 4 或 5 个与基本类无关的其他特征)。为什么我不能简单地说ReprMyClassA 并且只要它扩展了MyTrait 它并不关心它是否实现了其他特征?
  • 调整了最后一个示例 + 段落 - 它可能会对您有所帮助。
  • 那么,我是否必须为我拥有的每个特征排列创建一个中间复合特征? IE。每当我创建一个需要extend TraitA with TraitB with TraitC with TraitD 的新类时,我都必须创建一个新的CompoundABCDTrait
  • 除了上面的问题,为了让 CompoundABCDTrait 被传递给期望 CompoundABCTraitCompoundABDTraitCompoundBCDTrait 的函数,它不需要扩展所有函数吗?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-10-26
  • 1970-01-01
  • 2016-11-10
  • 2019-01-10
  • 2021-04-26
  • 2019-11-12
相关资源
最近更新 更多