Make42 Make42 - 1 year ago 82
Scala Question

May mix-in traits extend case classes from a technical perspective?

I read repeatedly on SO that case classes shall not be extended because a case class implements an equality method by default and that leads to issues of equality. However, if a trait extends a case class, is that also problematic?

case class MyCaseClass(string: String)
trait MyTrait extends MyCaseClass
val myCT = new MyCaseClass("hi") with MyTrait

I guess it boils down to the question, whether MyTrait is only forced to be mixable only into instantiations of MyCaseClass or whether MyTrait is inheriting the class members (field values and methods) of MyTrait and thus overwriting them. In the first case it would be okay to inherit from MyCaseClass, in the latter case it would not be okay. But which one is it?

To investigate, I advanced my experiment with

trait MyTrait extends MyCaseClass {
def equals(m: MyCaseClass): Boolean = false
def equals(m: MyCaseClass with MyTrait): Boolean = false
val myC = new MyCaseClass("hi")
myCT.equals(myC) // res0: Boolean = true

letting me to believe that the equals of MyCaseClass was used, not the one of MyTrait. This would suggest that it is okay for a trait to extend a case class (while it is not okay for a class to extend a case class).

However, I am not sure whether my experiment is legit. Could you shed some light on the matter?

Answer Source

Basically, trait can extend any class, so it's better to use them with regular classes.

Note that your equals contract is still broken regardless of your trick (note that standard Java's equals that is used ny default let's say in HashMaps is defined on Any):

scala> trait MyTrait extends MyCaseClass {
     |   override def equals(m: Any): Boolean = false
     | }
defined trait MyTrait

scala> val myCT = new MyCaseClass("hi") with MyTrait
myCT: MyCaseClass with MyTrait = MyCaseClass(hi)

scala> val myC = new MyCaseClass("hi")
myC: MyCaseClass = MyCaseClass(hi)

scala> myC.equals(myCT)
res4: Boolean = true

scala> myCT.equals(myC)
res5: Boolean = false

Besides, Hashcode/equals isn't the only reason...

Extending case class with another class is unnatural because case class represents ADT so it models only data - not behavior.

That's why you should not add any methods to it. So, after eliminating methods - a trait that can only be mixed with your class becomes nonsense as the point of using traits with case classes is to model disjunction (so traits are interfaces here - not mix-ins):

//your data model (Haskell-like):
data Color = Red | Blue

trait Color
case object Red extends Color
case object Blue extends Color

If Color could be mixed only with Blue - it's same as

data Color = Blue

Even if you require more complex data, like

//your data model (Haskell-like):
data Color = BlueLike | RedLike
data BlueLike = Blue | LightBlue
data RedLike = Red | Pink

trait Color  extends Red
trait BlueLike extends Color
trait RedLike extends Color
case class Red(name: String) extends RedLike //is OK
case class Blue(name: String) extends BlueLike //won't compile!!

binding Color to be only Red doesn't seem to be a good approach (in general) as you won't be able to case object Blue extends BlueLike

P.S. Case classes are not intended to be used in OOP-style (mix-ins are part of OOP) - they interact better with type-classes/pattern-matching. So I would recommend to move your complex method-like logic away from case class. One approach could be:

trait MyCaseClassLogic1 {
  def applyLogic(cc: MyCaseClass, param: String) = {}

trait MyCaseClassLogic2 extends MyCaseClassLogic {
  def applyLogic2(cc: MyCaseClass, param: String) = {}

object MyCaseClassLogic extends MyCaseClassLogic1 with MyCaseClassLogic2

You could use self-type or trait extends here but you can easily notice that it's redundant as applyLogic is bound to MyCaseClass only :)

Another approach is implicit class (or you can try more advanced stuff like type-classes)

implicit class MyCaseClassLogic(o: MyCaseClass) {
  def applyLogic = {}