Scala不可变对象和具有val字段的特征

时间:2010-08-11 14:51:34

标签: scala immutability scala-2.8 traits

我想仅使用不可变对象构建我的域模型。但我也想使用具有val字段的特征并将一些功能移到特征中。请看以下示例:

trait Versionable {
 val version = 0
 def incrementVersion = copy(version=version+1)
}

不幸的是,这样的代码不起作用 - 特征版本的复制方法是未知的。

我认为为每个特征和类生成复制方法会很好。这样的方法应该创建对象的浅拷贝,并使用与原始对象相同的类型返回它,并根据传递给方法的参数修改给定的字段。

所以在下面的例子中:

class Customer(val name: String) extends Versionable {
 def changeName(newName: String) = copy(name = newName)
}

val customer = new Customer("Scot")

customer.changeName("McDonnald")应该返回一个对象实例Customer(version = 0, name = "McDonnald")

customer.incrementVersion还应该返回一个对象实例Customer(version = 1, name = "Scot")

据我所知,Scala当前缺乏此类功能不允许使用不可变类和特征而不会使用特征字段污染类构造函数。在我的示例中,我不想将参数命名版本引入Customer类,因为版本处理的功能我想要封装在Versionable特性中。

我知道案例类中复制方法的功能以及使用默认参数在类中编写自己的复制方法的能力 - 但我认为此功能无法解决我的问题,因为无法使用此类复制方法在特质。现有功能的另一个缺点是使用复制方法的父类返回父类而不是实际复制的对象类。

我的问题:

1)您是否知道如何以优雅的方式处理上述示例。我对Scala很新,所以也许已经有了很好的解决方案。在我看来,优雅的解决方案应具有以下功能:

  • 不应使用反射

  • 不应使用序列化

  • 应该很快

  • 应该在编译时可以验证

2)您如何考虑编写编译器插件来为我上面的示例生成复制方法的代码?是否可以使用编译器插件来做到这一点?你有任何例子或提示怎么做?

5 个答案:

答案 0 :(得分:8)

您最干净的解决方案可能是从Versionable中删除一些实现逻辑,并将其向下推送到案例类(其中copy方法可供您使用)。为version属性提供默认值以完成设计。

trait Versioned {
  def version : Int
  def nextVersion = version + 1 
}

case class Customer(name: String, version : Int = 0) extends Versioned {
  def withName(newName: String) = copy(name = newName, version = nextVersion)
}

如果需要,还可以在版本编号的某处定义类型别名:

type Version = Int
val initialVersion = 0

trait Versioned {
  def version : Version
  def nextVersion = version + 1 
}

case class Customer(name: String, version : Version = initialVersion)
extends Versioned {
  def withName(newName: String) = copy(name = newName, version = nextVersion)
}

答案 1 :(得分:4)

这是另一种解决方案,就像OP的代码一样,不起作用。但是,它可以为扩展语言提供更简单(更普遍有用)的起点。

trait Versionable[T] {
   self: { def copy(version: Int): T } =>
   val version = 0
   def incrementVersion = copy(version = version + 1)
}

case class Customer(name: String, override val version: Int) 
      extends Versionable[Customer] {
   def changeName(newName: String) = copy(name = newName)
}

如果编译器将Customer类的copy方法识别为符合Versionable的自我类型注释中定义的方法,则代码将起作用,这似乎是使用命名和默认参数的自然方式。

答案 2 :(得分:3)

虽然您说过,但您不想使用案例类。以下是使用它们的解决方案:

case class Version(number: Int) {
  override def toString = "v" + number
  def next = copy(number+1)
}

case class Customer(name: String, version: Version = Version(0)) {
  def changeName(newName: String) = copy(newName)
  def incrementVersion = copy(version = version.next)
}

现在你可以这样做:

scala> val customer = new Customer("Scot")
customer: Customer = Customer(Scot,v0)

scala> customer.changeName("McDonnald")
res0: Customer = Customer(McDonnald,v0)

scala> customer.incrementVersion
res1: Customer = Customer(Scot,v1)

scala> customer // not changed (immutable)
res2: Customer = Customer(Scot,v0)

答案 3 :(得分:2)

这应该可以满足您的需求:

trait Request[T <: Request[T]] extends Cloneable {
  this: T =>
  private var rets = 0
  def retries = rets
  def incRetries:T = {
    val x = super.clone().asInstanceOf[T]
    x.rets = rets + 1
    x
  }
}

然后你可以像

一样使用它
case class Download(packageName:String) extends Request[Download]
val d = Download("Test")
println(d.retries) //Prints 0
val d2 = d.incRetries
println(d2.retries) //Prints 1
println(d.retries) //Still prints 0   

答案 4 :(得分:1)

很难看出这将如何工作并与Scala的语义保持一致 - 特别是在特征中定义的不可变字段的语义。考虑Versionable特征:

trait Versionable {
   val version = 0
}

此声明表示,除非被覆盖,否则版本字段将始终具有值0.更改version的值而不用使用特征字段污染类构造函数“(即未明确覆盖版本字段)会违反这些语义。