Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use a case classes when hierarchy is needed?

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?

like image 485
uzilan Avatar asked May 18 '12 07:05

uzilan


2 Answers

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"

like image 176
Christopher Chiche Avatar answered Oct 03 '22 15:10

Christopher Chiche


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?)

like image 44
KajMagnus Avatar answered Oct 03 '22 16:10

KajMagnus