Scala неизменяемые объекты и признаки с полями val

Я хотел бы построить мою модель домена, используя только неизменяемые объекты. Но я также хочу использовать черты с полями val и переместить некоторые функции в свойства. Посмотрите следующий пример:

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

К сожалению, такой код не работает - метод копирования неизвестен для свойства Versionable.

Я думаю, что было бы неплохо создать метод копирования для каждого признака и класса. Такой метод должен создавать неглубокую копию объекта и возвращать его с использованием того же типа, что и для исходного объекта, с заданным полем, модифицированным по аргументам, переданным методу.

Итак, в следующем примере:

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 не позволяет использовать неизменяемые классы и черты без использования класс-конструктора классов с полями признаков. В моем примере я не хочу вводить параметр с именем version в класс Customer, потому что функциональность обработки версий, которую я хочу инкапсулировать в черту Versionable.

Я знаю функциональность метода копирования в классах классов и возможность писать собственный метод копирования в классе с использованием параметров по умолчанию - но я думаю, что эта функциональность не решает мою проблему, потому что невозможно использовать такой метод копирования в чертах. Другим недостатком существующей функциональности является то, что родительский класс с использованием метода copy возвращает родительский класс, а не класс объекта, который фактически скопирован.

Мои вопросы:

1) у вас есть идея, как правильно выработать пример выше. Я новичок в Scala, поэтому, возможно, уже есть хорошее решение. На мой взгляд, элегантные решения должны иметь следующие функции:

  • не следует использовать отражение

  • не следует использовать сериализацию

  • должен быть быстрым

  • следует проверять во время компиляции

2) что вы думаете о написании плагина компилятора для генерации кода для метода копирования для моего примера выше? Можно ли это сделать с помощью плагина компилятора? У вас есть примеры или советы, как это сделать?

Ответ 1

Чистое решение, вероятно, должно отбросить некоторую логику реализации из Versionable и направить его вниз на стек типа в класс case (где метод 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)
}

Ответ 2

Здесь другое решение, которое, как и код 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 class copy как соответствующий методу, определенному в аннотации самостоятельного типа Versionable, что кажется естественным способом использования именованных и стандартных параметров.

Ответ 3

Хотя вы сказали, что вы не хотите использовать классы case. Вот решение, использующее их:

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)

Ответ 4

Это должно делать то, что вы ищете:

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   

Ответ 5

Трудно понять, как это будет работать и соответствовать семантике Scala - в частности, семантике неизменяемого поля, определенного в признаке. Рассмотрим свойство Versionable:

trait Versionable {
   val version = 0
}

В этом объявлении указано, что, если это не переопределено, поле версии всегда будет иметь значение 0. Чтобы изменить значение version "без загрязнения класса конструктора с полями свойств" (т.е. без явного переопределения поля версии), будет нарушено эти семантики.