0

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?

Make42
  • 12,236
  • 24
  • 79
  • 155
  • 2
    In my opinion, case classes have a dedicated meaning. They mean that you can model your domain objects in a concise and boiler-free manner. They provide hashcode, equality, apply and unapply which help us pattern match on those objects easily. Having a `trait` inherit a case class would feel weird and unnatural to me. Having said that, this problem generally feels like the [XY problem](http://meta.stackexchange.com/questions/66377/what-is-the-xy-problem). What are you trying to achieve? – Yuval Itzchakov Oct 26 '16 at 09:34
  • http://stackoverflow.com/questions/12854941/why-can-a-scala-trait-extend-a-class – dk14 Oct 26 '16 at 09:41
  • @YuvalItzchakov: I am having a class that is containing data. Depending on (complex) rules, they are of different kinds, noted by which traits are mixed in. With those attributes different behaviour is expected. Thus my traits get the behaviour and they extend the main data object making them rich. (I am aware that there is a discussion of [rich vs anemic models](http://stackoverflow.com/q/23314330/4533188), but [I favour the rich approach](http://www.martinfowler.com/bliki/AnemicDomainModel.html).) Question is whether I can make my data object a case class or leave it as a normal class. – Make42 Oct 26 '16 at 09:46
  • @YuvalItzchakov: However, this post was supposed to be about the concept really, not my specific case. It is more about learning than problem-solving. – Make42 Oct 26 '16 at 09:47
  • @dk14: Sorry, but not quite answering my question. The answer from your link, is what I already wrote in my question. I was also told on SO that one of the key differences between traits extending classes in opposite to traits self-referencing classes, is that in the former case the trait **is** that class (whatever that means). And because of that "whatever that means" I suspected that the trait might override methods. I want a specific clarification. – Make42 Oct 26 '16 at 09:51
  • 1
    In this case, it seems like you're doing the opposite. You have a case class extending a trait, not vice versa. A trait "enriching" a case class does make since to make and is a general pattern that is used. – Yuval Itzchakov Oct 26 '16 at 09:51
  • @YuvalItzchakov: No, the traits extends the class in the declaration (because the trait requires the classes fields for the methods) and later, during instantiation, I mix the traits into the class to enrich the class - just as in my example in the question. – Make42 Oct 26 '16 at 09:53
  • @YuvalItzchakov @Make42 I believe the bottom line here is that `case class`es are designed for FP which requires immutability. Martin Fowler's approach is OOP/OOD which is stateful by default - that's what he assumes in most of the part of his article by implying that service layer and business layer should have separate **states**. in FP (at least in my practice) we still separate domain logic from service-logic, but we also separate operations from data (in every layer), which is another matter. – dk14 Oct 26 '16 at 12:02
  • @dk: True, I am using a Hybrid approach FP + OOP. This is why I am using Scala in the first place. **But** my data object do **not** have state - as in FP! I am just adding functionality: If a data object does have specific attributes, it should provide (and only then the functionality). E.g. If the provided data by the user is complete a factory will return the (immutable) data object with a "complete" trait and only then this object has the ability to calculate a distance from another point to it. Otherwise the trait is not added by the factory. Everything here is immutable. – Make42 Oct 26 '16 at 12:19
  • Think about Apache Spark: RDDs are immutable. But only if a RDD is (also) Pair-RDD, some methods are possible to evoke. This is a little like having data locality: Bringing functionality to the data - but only the functionality that makes sense. – Make42 Oct 26 '16 at 12:37
  • @Make42 so if your data is immutable - it's better to use FP-approach and move functionality out of entities coz if you want to bring local functionality - you can just write a static function (explicit "this"), if you need dispatching - use pattern-matching(replaces virtual methods) in that function or type-classes (advanced overloading), for DSL - use implicit classes (so you could `a + b` etc.) – dk14 Oct 27 '16 at 15:50
  • It's same as RDD in Spark - the data-model and processors are separate - you basically apply some set of functions to an RDD, but neither RDD nor the entities inside them have any business-related methods. Mostly your lambdas are taking an entity type as a first parameter to transform data: `rdd.map((x: Entity) => ...)` or just `rdd.map(someFunction).filter(somePredicate)`. "Local" operations could be added with implicit class (or with type-classes in advanced case): `implicit class Rich(e: Entity){def + (e2: Entity) = ...}` and used like `rdd.reduce(_ + _)` – dk14 Oct 27 '16 at 15:50
  • @dk14: Whether functionality should be close to data or not might be a question of preference. However: In Matlab I separated them FP-style. In Java I put them together OOP-style. I even implemented the **same** algorithm/program in both - so I might have a good basis for judgement. My judgement so far is: I like functionality better to be with the data if they belong together and have functionality separated into FP-functions if they are (very) generic. This seems to produce programs that are easiest to be reasoned about. If there is a good reasoning for a different view, I'd love to read it. – Make42 Oct 31 '16 at 14:12
  • My point is that if you prefer OOP approach - you should not use case-classes. I prefer FP-approach and refactored a lot of real enterprise code from scal /java OOP to scalaFP, which improved not only readability but stability as well(it's easier to deal with concurrency when using FP). I consider true oop code much harder to reason about because of to factors: statefulness which causes a method to behave differently even with same parameters, runtime dispatching(virtual methods) because you don't know which method exactly was called (and you have to search potential candidates with IDE) – dk14 Nov 02 '16 at 06:22
  • talking about algorithms - it depends, if you're implementing some mathematical concept Fp is better as it gives you referential transparency. If you need to implement some calculations with maximum performance - procedural approach with mutable data is better. For complex parallel stateful calculations - actors and so on and so force. It's not a matter of preference - it's a matter of choosing the right instrument for your *current* task. And using this instrument in a way it's designed for (like case classes are designed to be immutable and not contain any functionality inside). – dk14 Nov 02 '16 at 06:31
  • @dk14: Thank you for giving your thought. What I am missing is actually my approach: immutable objects with methods. – Make42 Nov 02 '16 at 14:21
  • Why do you need methods? The only serious advantage of a java-style method is runtime dispatching (so called virtual methods) but scala has pattern matching mechanism to compensate (that's why it's called a *case* class). Besides, the original oop as it was implemented in Lisp CLOS) utilized multimethod concept with explicit "this" parameter - which is very similar to pattern matching in scala. Anyways, if you want an oop immutable class - scala allows you to do that as well: "class MyClass(val param1: String, val param2: String){def myVirtualMethod(param3: Int) {...}}". – dk14 Nov 02 '16 at 16:09
  • just use val and don't use case keyword. The point is that this approach (imho again) is redundant in most cases, but yes it's completely legit and useful sometimes. However if you abuse it, you might run into GoF-pattterns world, soIt might be a good idea to practice more with algebraic data types to learn their advantages/disadvantages as they often allow you to do things more simple, concise and keep it low-coupled without introducing additional unrelated entities as it usually happens with real oop; especially when it comes to domain-specific logic. – dk14 Nov 02 '16 at 16:22
  • @dk14: `class MyClass(val param1: String, val param2: String){def myVirtualMethod(param3: Int) {...}}` is how I am implementing my immutable objects. The question was - and I think it is answered - whether case class might be an alternative. – Make42 Nov 02 '16 at 16:58

2 Answers2

3

Basically, trait can extend any class, so it's better to use them with regular classes (OOP-style).

Anyway, your equals contract is still broken regardless of your trick (note that standard Java's equals is defined on Any, that is used by default let's say in HashMap or even ==):

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 (in OOD terms case classes are designed for anemic approach). 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

//Scala
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

//Scala
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 = {}
}

P.S.2 Anemic vs Rich. ADT is not precisely anemic model as it applies to immutable (stateless) data. If you read the article, Martin Fowler's approach is OOP/OOD which is stateful by default - that's what he assumes in most of the part of his article by implying that service layer and business layer should have separate states. in FP (at least in my practice) we still separate domain logic from service-logic, but we also separate operations from data (in every layer), which is another matter.

Community
  • 1
  • 1
dk14
  • 22,206
  • 4
  • 51
  • 88
  • "Basically, trait can extend any class (not just case class)" - my question is the opposite: Of course it can extend any class, but is it okay (as in good practise, not technically possible) to extend a class **even if** it is a case class? – Make42 Oct 26 '16 at 09:58
  • Read my answer carefully - it's not okay - as you either need a new class to extend your class (in order to use your trait) or you can't use your trait without that case class. Besides, you're not right (and your example is wrong too) about contract - as you can see `equals` is broken – dk14 Oct 26 '16 at 10:01
  • And `case class` usually doesn't have methods, it should model only **data** not behavior – dk14 Oct 26 '16 at 10:02
  • That MeClass cannot extend MyTrait is known. Why do I need a new class to extend my class? I am planing to use my traits only by mixing them into MyClass - just as the declaration of MyTrait commands. I don't understand why you bring MeClass into the picture in the first place. – Make42 Oct 26 '16 at 10:05
  • Then you gonna bring new methods to it. So, again, `case class` usually doesn't have methods, it should model only data not behavior – dk14 Oct 26 '16 at 10:07
  • @Make42 I've added more explanations and examples – dk14 Oct 26 '16 at 10:21
  • 1) Here is my setup, maybe that helps for understanding: I a have a data object D which must be of type D with A1 with B1 or D with A2 with B1 or D with A2 with B1 or D with A2 with B2. Where having an attribute (the traits) is adding functionality also. It is similar to a cake, but not the same. 2) As you can see: Yes it is (in part) for type safety. 3) Please note that "myCT" is supposed to mean "my Class with Trait" - please go with the wording of my question in your answer. – Make42 Oct 26 '16 at 10:24
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/126707/discussion-between-make42-and-dk14). – Make42 Oct 26 '16 at 10:29
  • @Make42 about `MyCT` - I updated the answer, to show that your contract is till broken - try: `MyC.equals(MyCT)` - it will give you controversal result – dk14 Oct 26 '16 at 10:30
  • I joined the chat, but you left the room :). Anyway, the point you shouldn't use case classes to implement functionality (especially cake-like) - you can use regular classes for that – dk14 Oct 26 '16 at 10:33
  • traits in case classes are not supposed to be mix-ins - they should be just interfaces – dk14 Oct 26 '16 at 10:33
  • Can you try again with the chat? (First time using it, probably did something wrong.) – Make42 Oct 26 '16 at 10:36
  • I've joined it again – dk14 Oct 26 '16 at 10:45
  • dk14: Can you write something in the chat? I wrote whether you can read me, but did not get an answer. – Make42 Oct 26 '16 at 10:59
  • I updated the answer to include the whole code from my REPL - as your approach to overriding `equals` wasn't correct – dk14 Oct 26 '16 at 11:07
1

Extending case classes is a bad practice (generally), because it has concrete meaning -- "data container" (POJO / ADT). For example, Kotlin does not allow to do that.

Also, if you really want some trait to extend case class, you'd better use requires dependency (to avoid pitfalls with cases classes inheritance):

scala> case class A()
defined class A

scala> trait B { self: A => }
defined trait B

scala> new B{}
<console>:15: error: illegal inheritance;
self-type B does not conform to B's selftype B with A
   new B{}
dveim
  • 3,381
  • 2
  • 21
  • 31
  • That is not possible in my case. MyTrait is given to (completely other) methods later on as a type. These other methods also require access to the data fields (which are declared in MyClass). Thus MyTrait **needs** to extend MyClass. – Make42 Oct 26 '16 at 10:14
  • @Make42 Do want to bake some cake? I mean, `scala` cake pattern. – dveim Oct 26 '16 at 10:17
  • Not really - I am hungry though (gotta make me a sandwich) ;-). I a have a data object D which must be of type `D with A1 with B1` or `D with A2 with B1` or `D with A2 with B1` or `D with A2 with B2`. Where having an attribute (the traits) is adding functionality also. It is similar to a cake, but not the same. – Make42 Oct 26 '16 at 10:21