【问题标题】:scala - idiomatic way to change state of classscala - 改变类状态的惯用方式
【发布时间】:2016-10-12 11:03:42
【问题描述】:

我有几个类都扩展了相同的特征,并且都共享应该改变它们状态的相互功能。但是我想知道是否有更好的方法来实现相同的功能。

例如:

trait Breed
case object Pincher extends Breed
case object Haski extends Breed

trait Foox{
  def age: Int
  def addToAge(i: Int): Foox 
}

case class Dog(breed: Breed, age: Int) extends Foox
case class Person(name: String, age: Int) extends Foox

我希望addToAge 将返回具有附加 int 的相同对象, 当然我可以为每个类实现相同的,这与 DRY 规则相矛盾:

case class Dog(breed: Breed, age: Int) extends Foox{
  def addToAge(i: Int) = copy(age = age + i)
}
case class Person(name: String, age: Int) extends Foox{
  def addToAge(i:Int) = copy(age = age + i)
}
  1. 有没有更好的方法来避免这种情况?

  2. 是否有一个选项可以避免在每个案例类中重新定义 age:Int 并保持其状态(年龄已在特征中定义)?

【问题讨论】:

  • 好吧,我真的很好奇这个。这个问题似乎有点类似于(但不是重复)thisthis。有谁知道那里给出的答案是否对这个问题有帮助?
  • 如果你试图改变同一个对象的状态,你可能违反了不变性原则。因此copy 似乎是一种方法。状态应该由使用这些对象的代码维护,即更新自己的状态以使用addToAge 返回的对象
  • 我不认为这里有一个简单的答案。我想到的最接近的想法是使用某种形式的镜头或使用无形库。您必须放弃 OO 并使用更多功能的抽象。例如,如果您有一个 map(),它只是改变了年龄,而让其他一切保持原样,您将在这种情况下实现您所需要的。
  • 您可以尝试copy 方法,就像在这个问题stackoverflow.com/questions/5341120/… 中一样,但这只会使您的案例类类型复杂化和限制。原因是copy只在case类本身中可用。
  • 有趣的问题......似乎围绕“复制”方法的魔法打破了通常的解决方案(即结构类型和宏)。您也许可以使用大量反射代码来实现这一点,但这可能不是您想要的。

标签: scala functional-programming


【解决方案1】:

一种可能的解决方案(可能涵盖某些用例)是使用来自shapeless 库的Lenses:

import shapeless._

abstract class Foox[T](
  implicit l: MkFieldLens.Aux[T, Witness.`'age`.T, Int]
) {
  self: T =>
  final private val ageLens = lens[T] >> 'age

  def age: Int
  def addToAge(i: Int): T = ageLens.modify(self)(_ + i)
}

case class Dog(breed: Breed, age: Int) extends Foox[Dog]
case class Person(name: String, age: Int) extends Foox[Person]

请注意,要创建Lens,您需要一个隐式MkFieldLens,因此将Foox 定义为abstract class 而不是trait 更容易。否则,您必须在每个孩子中编写一些代码来提供隐式。

另外,我认为没有办法避免在每个孩子中定义 age: Int。在构建实例时,您必须以某种方式提供年龄,例如Dog(Pincher, 5),所以你必须有那个构造函数参数来表示年龄。


更多解释:

借用Haskell Lens tutorial:

镜头是对某些数据类型的子部分的一级引用。 [...] 给定一个镜头,您基本上可以做三件事 可能想做的事

  1. 查看子部分
  2. 通过更改子部分来修改整体
  3. 将此镜头与另一个镜头结合使用,可以看得更深

第一个和第二个引起了镜头是吸气剂的想法 和像你一样的二传手可能在一个对象上。

修改部分可以用来实现我们想用age做的事情。

Shapeless 库提供了一种漂亮的、无样板的语法来为案例类字段定义和使用镜头。 The code example in the documentation 是不言自明的,我相信。

age 字段的以下代码来自该示例:

final private val ageLens = lens[???] >> 'age
def age: Int
def addToAge(i: Int): ??? = ageLens.modify(self)(_ + i)

addToAge 的返回类型应该是什么?它应该是调用此方法的子类的确切类型。这通常通过F-bounded polymorphism 实现。所以我们有以下内容:

trait Foox[T] { self: T => // variation of F-bounded polymorphism

  final private val ageLens = lens[T] >> 'age

  def age: Int
  def addToAge(i: Int): T = ageLens.modify(self)(_ + i)
}

T 在那里被用作孩子的确切类型,并且每个扩展 Foox[T] 的类都应该将自己提供为 T(因为自类型声明 self: T =>)。例如:

case class Dog(/* ... */) extends Foox[Dog]

现在我们需要使lens[T] >> 'age 行工作。

我们来分析>>方法的签名,看看它需要什么功能:

def >>(k: Witness)(implicit mkLens: MkFieldLens[A, k.T]): Lens[S, mkLens.Elem]
  1. 我们看到'age 参数被隐式转换为shapeless.WitnessWitness 表示特定值的确切类型,或者换句话说,一个类型级别的值。两种不同的文字,例如Symbols 'age'foo,有不同的见证人,因此可以区分它们的类型。

    Shapeless 提供了一种花哨的反引号语法来获得某个值的Witness。对于'age 符号:

    Witness.`'age`    // Witness object
    Witness.`'age`.T  // Specific type of the 'age symbol
    
  2. 根据第 1 项和>> 签名,我们需要为类T(子case class)和字段'age 提供一个隐式MkFieldLens

    MkFieldLens[T, Witness.`'age`.T]
    

    age 字段的类型也应为 Int。可以用 shapeless 中常见的Aux pattern 来表达这个要求:

    MkFieldLens.Aux[T, Witness.`'age`.T, Int]
    

为了更自然地提供这种隐式,作为隐式参数,我们必须使用abstract class 而不是trait

【讨论】:

  • 这与我的想法相似。我们是否也可以添加导入以使某人更容易在 repl 中重现它?
  • @marios 好的,已添加。 shapeless._ 足以让它工作。此外,它需要原始问题中的Breed 定义。
  • @Kolmar,谢谢,这正是我要找的,你能详细说明一下这个巫毒吗:)?
  • @igx 添加了一些解释
猜你喜欢
  • 2015-02-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-04-03
  • 2021-05-29
  • 2015-01-07
  • 1970-01-01
相关资源
最近更新 更多