11

I would like to construct my domain model using immutable objects only. But I also want to use traits with val fields and move some functionality to traits. Please look at the following example:

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

Unfortunatelly such code doesn't work - copy method is unknown for trait Versionable.

I think that it would be nice to have copy method generated for every trait and class. Such method should create shallow copy of object and return it using the same type as for original object with given field modified accoring to arguments passed to method.

So in the following example:

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

val customer = new Customer("Scot")

customer.changeName("McDonnald") should return an object instance Customer(version = 0, name = "McDonnald")

and

customer.incrementVersion should also return an object instance Customer(version = 1, name = "Scot")

As far as I know current lack of such functionality in Scala doesn't allow to use immutable classes and traits without polluting class constructor with trait fields. In my example I don't want to introduce parameter named version to Customer class because functionality of version handling I want to have encapsulated in Versionable trait.

I know functionality of copy method in case classes and ability to write own copy method in class using default parameters - but I think that this functionality doesn't solve my problem because it is not possible to use such copy method in traits. Another drawback of existing functionality is that parent class using copy method returns parent class and not class of object that is actually copied.

My questions:

1) do you have idea how to handle above example in elegant way. I'm quite new to Scala so maybe there is good solution already. In my opinion elegant solutions should have following features:

  • should not use reflection

  • should not use serialization

  • should be fast

  • should be verifiable in compile time

2) what do you think about writing compiler plugin to generate code for copy method for my above example? Is it possible to do that using compiler plugin? Do you have any examples or tips how to do that?

4

5 回答 5

8

您最干净的解决方案可能是从 中删除一些实现逻辑Versionable,并将其从类型堆栈下推到案例类(copy您可以在其中使用该方法)。给版本属性一个默认值来完成设计。

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)
}
于 2010-09-14T14:05:40.597 回答
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 的自类型注释中定义的方法,则代码将起作用,这似乎是使用命名参数和默认参数的自然方式。

于 2010-08-12T15:56:06.227 回答
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)
于 2010-08-12T07:28:06.003 回答
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   
于 2013-05-02T15:59:45.507 回答
1

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

trait Versionable {
   val version = 0
}

该声明说,除非被覆盖,否则版本字段将始终具有值 0。更改version“不使用 trait 字段污染类构造函数”的值(即不显式覆盖版本字段)将违反这些语义。

于 2010-08-11T17:18:31.153 回答