19

I know that you're not allowed to inherit from case classes but how would you do when you really need to? We have two classes in a hierarchy, both contain many fields, and we need to be able to create instances of both. Here's my options:

  • If I'd make the super class a usual class instead of a case class - I'd lose all the case class goodness such as toString, equals, hashCode methods etc.
  • If I keep it as a case class, I'd break the rule of not inheriting from case classes.
  • If I use composition in the child class - I'd have to write lots of methods and redirect them to the other class - which would mean lots of work and would feel non-Scalaish.

What should I do? Isn't it quite a common problem?

uzilan
  • 2,554
  • 2
  • 31
  • 46

3 Answers3

15

Yes this is quite a recurrent problem, what I would suggest is to create a trait with all parent properties, create a case class which just implements it and then another one which inherits of it with more properties.

sealed trait Parent {
  /* implement all common properties */
}

case class A extends Parent

case class B extends Parent {
  /*add all stuff you want*/
}

A good way of seeing it is a tree, traits are nodes and case classes are leaves.

You can use a trait or an abstract class depending on your needs for the parent. However, avoid using a class because you would be able to create instances of it, which would not be elegant.

EDIT: As suggested in comments, you can seal the trait in order to have exceptions at compilation if not all case classes are covered in a pattern matching. It is for example explained in chapter 15.5 of "Programming in Scala"

Christopher Chiche
  • 15,075
  • 9
  • 59
  • 98
6

What about replacing inheritance with delegation?

If your two classes in the hierarchy have many shared fields, then delegation could reduce the amount of boilerplate code? Like so:

case class Something(aa: A, bb: B, cc: C, payload: Payload)

sealed abstract class Payload

case class PayloadX(xx: X) extends Payload
case class PayloadY(yy: Y) extends Payload

And then you would create Something instances like so:

val sth1 = Something('aa', 'bb', 'cc', PayloadX('xx'))
val sth2 = Something('aa', 'bb', 'cc', PayloadY('yy'))

And you could do pattern matching:

sth1 match {
  case Something(_, _, _, PayloadX(_)) => ...
  case Something(_, _, _, PayloadY(_)) => ...
}

Benefits: (?)

  • When you declare PayloadX and PayloadY, you don't have to repeat all fields in Something.

  • When you create instances of Something(... Payload(..)), you can reuse code that creates Something, both when you create a Something(... PayloadX(..)) and ...PayloadY.

Drawbacks: (?)

  • Perhaps PayloadX and Y in your case are actually true subclasses of Something, I mean, in your case, perhaps delegation is semantically wrong?

  • You would have to write something.payload.whatever instead of simply something.whatever (I suppose this might be good or bad depending on your particular case?)

KajMagnus
  • 11,308
  • 15
  • 79
  • 127
  • 1
    Another drawback: you won't be able to talk about specific types in method signatures anymore, it will always be "Something". – Victor Basso Jun 30 '17 at 23:18
4

I explored this issue as well, AFAIK, the best you're going to get is:

Have each case class extend from a common trait that defines abstract properties each case class must implement

It doesn't remove the boilerplate (at all), but defines a contract your case classes must adhere to, while not losing case class feature set...

virtualeyes
  • 11,147
  • 6
  • 56
  • 91