user79074
user79074

Reputation: 5270

Case class copy does not maintain state of an inherited trait

Say I define a trait and case class that extends it:

    trait A {
        var list: List[Int] = List()
        def add(i: Int) = list = i :: list
    }

    case class B(str: String) extends A

If I then create an instance of B, modify the list and make a copy of it, the copy takes the values of the list as defined in trait A:

    val b = B("foo")
    b.add(3)
    println("B: " + b.list)
    println("Copied: " + b.copy(str = "bar").list)

Output:

B: List(3)

Copied: List()

I know this is probably not good functional programming practice anyway, but is there a way I can force the copy of B to take the altered version of the list for instance b?

Upvotes: 2

Views: 264

Answers (2)

EdgeCaseBerg
EdgeCaseBerg

Reputation: 2841

You can accomplish this by declaring the list in the case class, and satisfy your trait's type by declaring a method. So:

trait A {
    def list: List[Int]
}

case class B(i: Int, var list: List[Int]) extends A

And then your example:

scala> val b = B(2, List(1))
b: B = B(2,List(1))

scala>  b.list = List(3)
b.list: List[Int] = List(3)

scala>  println("B: " + b.list)
B: List(3)

scala> println("Copied: " + b.copy(i = 4).list)
Copied: List(3)

I don't know what you're doing that requires a mutable list in your class, but I imagine that even though this satisfies your example it might not satisfy whatever problem you're working on since:

scala> val a: A = b
a: A = B(2,List(3))

scala> a.list
res2: List[Int] = List(3)

scala> a.list = List(4)
<console>:15: error: value list_= is not a member of A
       a.list = List(4)

When trying to view an instance of B as an A you won't be able to assign the list type. If you were dealing with an instance of A you'd have to do something like

scala> a match {
     | case itsAB: B => itsAB.list = List(4); itsAB
     | case other => other
     | }
res3: A = B(2,List(4))

But either way, this is how you'd do it. Either that or you can follow phongnt's advice to create a non-case class and define a copy method yourself.

In a comment you said:

I failed to mention that the trait attribute needs to be modifiable with a reference to the trait itself

But this sounds like global mutable state to me. If you really need that, and I encourage you to not to then you can use an object:

object A {
   @volatile
   var list = List.empty[Int]
}

But I doubt this gets at what you're expressing in your question either since there is not an individual list still per instance here. So... looking at your updated question, I can only guess that something like this is what you're trying to accomplish:

object A {
   @volatile
   var list = List.empty[Int]
}

trait A {
   def list = A.list
   def add(i: Int) = A.list = i :: A.list
}

case class B(str: String) extends A

Which lets you:

scala> val b = B("hi")
b: B = B(hi)

scala> b.add(0)

scala> b.add(1)

scala> A.list
res4: List[Int] = List(1, 0)

But again, this is global mutable state, it's not a good thing. It's not threadsafe. So if there's a way for you to figure out your problem without doing this... I'd encourage you to do so.

Upvotes: 4

phongnt
phongnt

Reputation: 505

The copy method is generated for you by the compiler when you declare a case class and it is only parameterised with the variables you define in the case class

Hence, you can definitely have copy method do that for you by declaring the variable list in the case class e.g. case class B(i: Int, list: List[Int]). You will also need override val modifier to satisfy scalac. However, scalac doesn't allow you to override a mutable variable, so you need to change the trait too, for example

trait A {
  val list: List[Int] = List(1)
}

case class B(i: Int, override val list: List[Int]) extends A 

Upvotes: 3

Related Questions