Dobes Vandermeer
Dobes Vandermeer

Reputation: 8820

Scala: How can I make my immutable classes easier to subclass?

I've recently created an immutable class supporting operations like +, -, etc. that returns a new instance of that class when it is changed.

I wanted to make a subclass of that class to add a bit of state and functionality but now I'm running into a problem in that all the original class's methods return instances of itself rather than the subclass.

Based on my current limited knowledge of Scala I can come up with this:

class Foo(val bar:Int) { 
  def copy(newBar:Int) = new Foo(newBar)
  def + (other:Foo):This = copy(this.bar + other.bar) 
}
class Subclass(barbar:Int) extends Foo(barbar) { 
  override def copy(newBar:Int) = new Subclass(newBar)
  override def + (other:Subclass) = super.+(other).asInstanceOf[Subclass]
}

The problem here is quite obvious - all operations of the superclass that return a new instance of have to be re-defined in the subclass with a cast.

At first "this.type" seemed promising but "this.type" only includes "this" and not any other object of the same type.

Is there a standard pattern for making immutable classes easy to subclass? Something like:

class Foo(val bar:Int) { 
  def copy(newBar:Int):SameType = new Foo(newBar)
  def + (other:Foo) = copy(this.bar + other.bar) 
}
class Subclass(barbar:Int) extends Foo(barbar) { 
  override def copy(newBar:Int):SameType = new Subclass(newBar)
  override def + (other:Subclass) = super.+(other).asInstanceOf[Subclass]
}

This particular approach would require the compiler to require that all subclasses implement a copy() method that returns the same type as that subclass, which would be perfectly fine with me. However, I don't think any such thing exists in Scala at this time.

Some work-arounds that come to mind are:

  1. Use delegation - but of course I'd still be re-implementing all the methods as delegate calls
  2. Use implicit types to add operations instead of subclassing
  3. Use a mutable data structure. This is probably the simplest and quickest solution, but I'd lose the benefits of using immutable data structures (which I'm still hoping to learn more about).

I'm sure this has been discussed many times already and I apologize for asking again. I did Google for a duplicate question without success, so my search terms must have been poorly constructed.

Thanks in advance,

Dobes

Upvotes: 14

Views: 800

Answers (1)

Jean-Philippe Pellet
Jean-Philippe Pellet

Reputation: 60006

You could use an implementation trait, like the collection classes do, which is parametrized by the concrete type. E.g., something like:

trait FooLike[+A] {
  protected def bar: Int

  protected def copy(newBar: Int): A
  def +(other: Foo): A = copy(bar + other.bar)
}

class Foo(val bar: Int) extends FooLike[Foo] {
  protected def copy(newBar: Int): Foo = new Foo(newBar)
}

class Subclass(barbar: Int) extends Foo(barbar) with FooLike[Subclass] {
  protected def copy(newBar: Int): Subclass = new Subclass(newBar)
}

Upvotes: 9

Related Questions