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) что вы думаете о написании плагина компилятора для генерации кода для метода копирования для моего примера выше? Возможно ли это сделать с помощью компилятора? У вас есть примеры или советы, как это сделать?

5 ответов

Самое чистое решение - это, вероятно, отказаться от логики реализации 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)
}

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

Хотя вы сказали, что вы не хотите использовать 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)

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

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   

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

trait Versionable {
   val version = 0
}

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

Другие вопросы по тегам