Scala case class inheritance

107,558

Solution 1

My preferred way of avoiding case class inheritance without code duplication is somewhat obvious: create a common (abstract) base class:

abstract class Person {
  def name: String
  def age: Int
  // address and other properties
  // methods (ideally only accessors since it is a case class)
}

case class Employer(val name: String, val age: Int, val taxno: Int)
    extends Person

case class Employee(val name: String, val age: Int, val salary: Int)
    extends Person


If you want to be more fine-grained, group the properties into individual traits:

trait Identifiable { def name: String }
trait Locatable { def address: String }
// trait Ages { def age: Int }

case class Employer(val name: String, val address: String, val taxno: Int)
    extends Identifiable
    with    Locatable

case class Employee(val name: String, val address: String, val salary: Int)
    extends Identifiable
    with    Locatable

Solution 2

Since this is an interesting topic to many, let me shed some light here.

You could go with the following approach:

// You can mark it as 'sealed'. Explained later.
sealed trait Person {
  def name: String
}

case class Employee(
  override val name: String,
  salary: Int
) extends Person

case class Tourist(
  override val name: String,
  bored: Boolean
) extends Person

Yes, you have to duplicate the fields. If you don't, it simply would not be possible to implement correct equality among other problems.

However, you don't need to duplicate methods/functions.

If the duplication of a few properties is that much of an importance to you, then use regular classes, but remember that they don't fit FP well.

Alternatively, you could use composition instead of inheritance:

case class Employee(
  person: Person,
  salary: Int
)

// In code:
val employee = ...
println(employee.person.name)

Composition is a valid and a sound strategy that you should consider as well.

And in case you wonder what a sealed trait means — it is something that can be extended only in the same file. That is, the two case classes above have to be in the same file. This allows for exhaustive compiler checks:

val x = Employee(name = "Jack", salary = 50000)

x match {
  case Employee(name) => println(s"I'm $name!")
}

Gives an error:

warning: match is not exhaustive!
missing combination            Tourist

Which is really useful. Now you won't forget to deal with the other types of Persons (people). This is essentially what the Option class in Scala does.

If that does not matter to you, then you could make it non-sealed and throw the case classes into their own files. And perhaps go with composition.

Solution 3

case classes are perfect for value objects, i.e. objects that don't change any properties and can be compared with equals.

But implementing equals in the presence of inheritance is rather complicated. Consider a two classes:

class Point(x : Int, y : Int)

and

class ColoredPoint( x : Int, y : Int, c : Color) extends Point

So according to the definition the ColorPoint(1,4,red) should be equal to the Point(1,4) they are the same Point after all. So ColorPoint(1,4,blue) should also be equal to Point(1,4), right? But of course ColorPoint(1,4,red) should not equal ColorPoint(1,4,blue), because they have different colors. There you go, one basic property of the equality relation is broken.

update

You can use inheritance from traits solving lots of problems as described in another answer. An even more flexible alternative is often to use type classes. See What are type classes in Scala useful for? or http://www.youtube.com/watch?v=sVMES4RZF-8

Share:
107,558
Andrea
Author by

Andrea

Updated on July 08, 2022

Comments

  • Andrea
    Andrea almost 2 years

    I have an application based on Squeryl. I define my models as case classes, mostly since I find convenient to have copy methods.

    I have two models that are strictly related. The fields are the same, many operations are in common, and they are to be stored in the same DB table. But there is some behaviour that only makes sense in one of the two cases, or that makes sense in both cases but is different.

    Until now I only have used a single case class, with a flag that distinguishes the type of the model, and all methods that differ based on the type of the model start with an if. This is annoying and not quite type safe.

    What I would like to do is factor the common behaviour and fields in an ancestor case class and have the two actual models inherit from it. But, as far as I understand, inheriting from case classes is frowned upon in Scala, and is even prohibited if the subclass is itself a case class (not my case).

    What are the problems and pitfalls I should be aware in inheriting from a case class? Does it make sense in my case to do so?

  • Andrea
    Andrea over 11 years
    I understand and agree with that. So, what do you suggest should be done when you have an application that deals with, say, employers and employees. Assume that they share all fields (name, address, and so on), the only difference being in some methods - for instance one may want to define Employer.fire(e: Emplooyee) but not the other way round. I would like to make two different classes, since they actually represent different objects, but I also dislike the repetition that arises.
  • virtualeyes
    virtualeyes over 11 years
    got an example of type class approach with the question here? i.e. in regard to case classes
  • virtualeyes
    virtualeyes over 11 years
    where is this "without code duplication" that you speak of? Yes, a contract is defined between case class and its parent(s), but you're still typing out props X2
  • Malte Schwerhoff
    Malte Schwerhoff over 11 years
    @virtualeyes True, you still have to repeat the properties. You don't have to repeat methods, though, which usually amount to more code than the properties.
  • virtualeyes
    virtualeyes over 11 years
    yeah, was just hoping to work around the duplication of properties -- another answer hints at type classes as a possible workaround; not sure how, however, seems more geared toward mixing in behaviour, like traits, but more flexible. Just boilerplate re: case classes, can live with it, would be pretty incredible if it were otherwise, could really hack out great swaths of property definitions
  • Malte Schwerhoff
    Malte Schwerhoff over 11 years
    @virtualeyes I completely agree that it would be great if property repetition could be avoided in an easy way. A compiler plugin could surely do the trick, but I wouldn't call that an easy way.
  • Jens Schauder
    Jens Schauder over 11 years
    @virtualeyes One could have completely independent types for the various kinds of Entities and provide Type Classes to provide the behaviour. These Type Classes could use inheritance as much as useful, since they aren't bound by the semantic contract of case classes. Would it be useful in this question? Don't know, the question isn't specific enough to tell.
  • virtualeyes
    virtualeyes over 11 years
    @JensSchauder it would appear that traits provide the same thing in terms of behavior, just less flexible than type classes; I'm getting at the non-duplication of case class properties, something that traits or abstract classes would normally help one to avoid.
  • ses
    ses almost 11 years
    What would you recommend for this case: abstract class Form; case class Verb extends Form; case class Noun extends Form ? case class in this format is deprecated
  • Malte Schwerhoff
    Malte Schwerhoff almost 11 years
    @ses Either use case object Verb extends Form` or case classes with explicit empty constructors, e.g., case class Verb() extends Form
  • BAR
    BAR about 9 years
    I think that the def name in the trait needs to be val name. My compiler was giving me unreachable code warnings with the former.
  • Daniel
    Daniel almost 9 years
    @virtualeyes I think that avoiding code duplication is not only about writing less. For me, it is more about not having the same piece of code in different parts of your application without any connection between them. With this solution, all the sub-classes are tied to a contract, so if the parent class changes, the IDE will be able to help you to identify the parts of the code you need to fix.