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

发布于 2024-09-14 02:05:30 字数 1279 浏览 7 评论 0原文

我想仅使用不可变对象构建我的域模型。但我也想将特征与 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("McDonald") 应返回一个对象实例 Customer(version = 0, name = "McDonald")

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

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

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

我的问题:

1)您知道如何以优雅的方式处理上面的示例吗?我对 Scala 很陌生,所以也许已经有很好的解决方案了。在我看来,优雅的解决方案应该具有以下特征:

  • 不应该使用反射

  • 不应该使用序列化

  • 应该很快

  • 应该在编译时可验证

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

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?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

灯下孤影 2024-09-21 02:05:30

最干净的解决方案可能是从版本化中删除一些实现逻辑,并将其从类型堆栈中推送到案例类(其中您可以使用复制方法)。为版本属性指定默认值以完成设计。

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)
}

You cleanest solution is probably to drop some implementation logic from Versionable, and push it down the type stack to a case class (where the copy method will be available to you). Give the version property a default value to complete the design.

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)
}

If you want, you can also define a type alias somewhere for the version numbering:

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)
}
南冥有猫 2024-09-21 02:05:30

这是另一个解决方案,与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 的自类型注释中定义的方法(这似乎是使用命名参数和默认参数的自然方式),则该代码将起作用。

Here's another solution that, like the OP's code, doesn't work. However, it may provide a simpler (and more generally useful) starting point for extending the language.

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)
}

The code would work if the compiler recognized the Customer class's copy method as conforming to the method defined in Versionable's self-type annotation, which seems like a natural way to use named and default parameters.

无所谓啦 2024-09-21 02:05:30

尽管您说过,您不想使用案例类。这是使用它们的解决方案:

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)

Although you said, you don't want to use case classes. Here is a solution using them:

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)
}

Now you can do this:

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)
巾帼英雄 2024-09-21 02:05:30

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

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   

This should do what you are looking for:

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
  }
}

Then you can use it like

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   
故人如初 2024-09-21 02:05:30

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

trait Versionable {
   val version = 0
}

此声明表示,除非被覆盖,版本字段的值将始终为 0。要更改 version 的值,“不会用特征字段污染类构造函数”(即没有显式地覆盖版本字段)将违反这些语义。

It's difficult to see how this would work and be consistent with Scala's semantics -- in particular, the semantics of an immutable field defined in a trait. Consider the Versionable trait:

trait Versionable {
   val version = 0
}

This declaration says that, unless overridden, the version field will always have the value 0. To change the value of version "without polluting the class constructor with trait fields" (i.e. without explicitly overriding the version field) would violate these semantics.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文