Scala immutable objects and traits with val fields
Asked Answered
F

5

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?

Forthright answered 11/8, 2010 at 14:51 Comment(4)
Are you aware that case classes are endowed by the compiler with a copy method such as you describe? Case classes cannot (technically, should not) derive from other case classes. In particular, the derived case class will inherit the super-(case)-class's copy method.Iolite
Yes, I know that and not going to use case classes to solve described problem. I am trying to find solution for writing immutable classes with traits that are not only interfaces as in Java but adds implementation. "Mutator" methods in such traits should return copy of object with modified "mutated" fields but how to do this in elegant way?Forthright
Perhaps this question can provide some useful ways for you to deal with this: #3007964Amazon
I added a related question: #3472208 It's also related to my (non-working) solution below.Hals
B
8

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)
}
Blunge answered 14/9, 2010 at 14:5 Comment(0)
H
4

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.

Hals answered 12/8, 2010 at 15:56 Comment(0)
M
3

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)
Mistrial answered 12/8, 2010 at 7:28 Comment(0)
C
2

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   
Conlee answered 2/5, 2013 at 15:59 Comment(1)
Wow, that is crazy. From what I can see this have an immutable behaviour. But I just want to double check: could this solution have any issues with thread safety?Biggerstaff
H
1

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.

Hals answered 11/8, 2010 at 17:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.