Lai Yu-Hsuan
Lai Yu-Hsuan

Reputation: 28081

Immutable copy-update in abstract class

I want my class to be both immutable and updatable. This is my boilerplate:

case class A(a: Int, b: Int) {
  def withB(newB: Int) = copy(b=newB)
}

It works as expected. The problem is, what if I hope the client to derive A but not directly instantiate it? This:

abstract case class A(a: Int, b: Int) {
  def withB(newB: Int) = copy(b=newB)
}

Doesn't work. (Of course not, how could A copy itself without instantiating?)

Upvotes: 2

Views: 216

Answers (2)

wheaties
wheaties

Reputation: 35970

You need to start looking at other patterns to help you out with updating immutable objects. Start taking a look at Lens types. They work like this:

trait Lens[A,B]{
  self =>

  def get(obj: A): B
  def set(obj: A, value: B): A

  def andThen(that: Lens[B, C]): Lens[A,C]{
    def get(obj: A) = that get (self get obj)
    def set(obj: A, value: C) = self set (obj, that set(self get obj, value))
  }
}

So that you can define a "get" and "set" which is both in keeping with the pure (they work by making a copy) and composable nature of most things in Scala. Hence, in your example, you would not define the withTwo method on the object itself but create a Lens for it.

Using a Lens:

case class Bar(c: Int)
case class Foo(a: Int, b: Bar)

object LensFooBar extends Lens[Foo, Bar]{
  def get(foo: Foo) = foo.b
  def set(foo: Foo, bar: Bar) = foo.copy(b = bar)
}
object LensBarC extends Lens[Bar, Int]{
  def get(bar: Bar) = bar.c
  def set(bar: Bar, value: Int) = bar.copy(c = value)
}

val myFirst = Foo(1, Bar(2))
val updated = LensFooBar.set(myFirst, Bar(3))

val lenser = LensFooBar andThen LensBarC
val updatedFurther = lenser set(myFirst, 3)

Both lens are doing the same thing but this should illustrate not only how to work with immutable nested structures but also how to work with immutable objects outside of placing a method for each field you want to change within the class.

You can see this Scala Days 2013 talk or read about how Scalaz implements Lens here.

Upvotes: 1

Alex Yarmula
Alex Yarmula

Reputation: 10667

what if I hope the client to derive A but not directly instantiate it?

You can use the Builder pattern to do it. Your builder would be a (case) class that has everything necessary to create A, and you can pass it to the place where A needs to be instantiated.

For instance,

case class ABuilder(one: Int = 1, two: String = "") {
  def withOne(v: Int) = copy ...
  def withTwo(v: String) = copy ...
  def build(): A = ...
}

Upvotes: 0

Related Questions