Overriding methods in case class

I have a case class which inherits from an abstract class defined in my application.
What is the rule about which methods I can dependently override or expect to be inherited from the abstract class.

For example, I believe that equals and hashCode DO NOT inherit from the abstract class, because the case class defines its own if the there is not one explicitly given between the {...} of the case class. Right?

I suppose I can just write the case class like the following if I want it to get its equals and hashCode from the parent class?

case class X(...) extends Y {
  override def equals(that:Any) = super.equals(that)
  override def hashCode():Int = super.hashCode()
  ...
}

See the spec.

Every case class implicitly overrides some method definitions of class scala.AnyRef unless a definition of the same method is already given in the case class itself or a concrete definition of the same method is given in some base class of the case class different from AnyRef.

Here lie dragons.

The first rule is “NEVER override equals or hashCode in a case class”. If you need to override them, then revert to a normal class.

And if you decide to implement equals and hashCode on a normal class (if you override one, you must override BOTH), here’s a StackOverflow Answer I created when I hit this exact same issue in the last year or so.

2 Likes

Just curious, could you explain the reasoning? I’m certainly with you there - I don’t think I’ve ever overridden equals/hashCode in a case class, and whenever I find myself considering to tinker with the generated parts of a case class (which rarely happens), I will trace back and check whether a vanilla class with an unapply method will do. However, the spec seems to anticipate this technique, and I cannot come up with a caveat stronger than “it makes things more complicated and confusing”, which doesn’t seem to directly translate to “never”…

I am repeating the strong advice I got from multiple other Scala software engineers over the last +10 years. And because I am hard-headed, I didn’t heed the advice initially, overrode them in case classes anyway, and then ended up on a long refactoring tangent once I finally learned the lesson through explicit complications and failures.

Here’s the SO Answer from one of the Scala software engineers I respect and admire most:

1 Like

If you should never override equals in a case class, then why does the syntax allow you to do so? Shouldn’t the compiler refuse to compile this if it is wrong? Don’t gotchas like this just make the language more difficult and confusing to use?

1 Like

If I want to convert my class(es) from case class(es) to normal classes, is it really an unapply that I need to implement? Isn’t there another method I have to implement (additionally or instead) if the class is defined with a varargs initialisation parameter?

case class SOr(override val tds: SimpleTypeD*) extends SCombination {
  override def equals(that:Any):Boolean = {
    super.equals(that)
  }
  override def hashCode():Int = {
    super.hashCode()
  }
...
}

Because Scala doesn’t want to be too strict, it allows you to go any kind of crazy thing for better or worse.

However, each sub-community has come to define its own rules. And not overriding equals & hashCode of case classes is one that most people seem to agree with (plus making case classses final)

1 Like

So it sounds like case classes are something that seemd like a good idea at the beginning of the language definition, but have become a source of problems 15 years on? And potentially should simply be avoided altogether?

I would agree with you, most certainly. I wish there was a compiler flag to support that. And I would love to see it codified within the compiler itself. OR…if the guidance has changed, what the idiomatic Scala suggested way to solve the problem should be.

The Scala community is kind of divided about the validity of exploiting the case class, anyway. Those that tend to be more FP inclined tend to dislike how it is too much OOP. And the OOP inclined, tend to treat it too much as a typical OO class missing its FP value as a product type.

The case class is an intentional integration of both OOP and FP which is leveraged heavily by the Scala collections API. I personally have found it to be extraordinarily useful over the years, despite warts like this.

I really like the Sum type being a Enum and the product type being a Case Class. This has given Scala a much cleaner definition around these two fundamental data type patterns. I suspect over time, they will become more rigorously encapsulated so as to make the language easier and simpler to use.

1 Like

Uhm no…
Not sure from where you draw that conclusion.

Just use them for what they are intended for and you are ok.
I don’t want to repeat myself because we both have discussed this several times. Your use of cases classes and sealed traits is far from idiomatic, that is not to say your code is wrong, but you would be better with just regular classes (rather than trying to mix OOP wizardry with a plain ADT) as we have suggested you multiple times.

1 Like

I abandoned the ADT long ago because of the single file restriction.

There are those who would agree with this sentiment.

Like any tool, it has places where it is useful, and others where it is a forced fit. The current state of case classes definitely fall into this classification.

I use case classes for dead simple DAOs. As soon as I am starting to incorporate any sophistication, OOP-wise and/or FP-wise, I flip over to regular classes. And this will then lead you to…

How do I go about reconstituting the parts of the case class the compiler was implementing on my behalf? At present, I don’t know of a single place where you can see all of the interface and implementation details upon which the compiler generates the case class code.

1 Like

yes, so it is it pretty opaque to me how to convert a case class to a normal class once I go beyond the simple cases.

1 Like

I agree. It’s a gap that’s probably worth filling, kind of like what I did with producing the idiomatic equals/hashCode StackOverflow Answer. And if I get some time in the next month or so, I might take a stab at that given the two of us are hardly the first ones to bump up against this.

In fact, the lack of this specification likely is WHY so many end up abusing the case class well beyond its intended stupid simple use cases (primarily as a DAO for the Scala collections API). I don’t know how many times I have started to convert from a case class to a class and paused because I was about to have to figure out how the equals/hashCode/unapply/toTuple/tupled/etc. methods need to be specified and implemented. It definitely has had impacts on my coding flow productivity.

1 Like

It depends on how you want to use it. Basically, a case class is kind of a “macro” that generates a class with equals/hashCode/toString/copy plus accessors and a companion object with apply/unapply - and all of these are guaranteed to have consistent semantics based on the constructor arguments. You could just as well craft the whole thing by hand, it just involves lots of repetitive overhead.

If you start overriding selected methods in this construct, you’re bound to get inconsistent behavior - or at least unexpected to somebody who just recognizes this thing as a case class. In this case one should contemplate whether one actually needs all the compiler-generated scaffolding. In my case it turned out that if I ever wanted to deviate from the case class way, then most of the time the one thing I actually really wanted was pattern matching, i.e. unapply - all the other features were rather nice to have or completely unused. YMMV.

2 Likes

I didn’t realize case class generates a toString. I almost always define my own toString in every case class.

The thing that I mostly need (I think in this particular application) is varargs apply and unapply methods with work.

This reply may be slightly off topic, but I just wanted to point out what I consider one of the main benefits of case classes.

In my first few years of using Scala, I thought that case classes were only for tiny classes such as point or date. I eventually realized, however, that they are very useful even for larger classes. In fact, I now use case classes for nearly every class the I write myself and actually instantiate.

One of the main benefits of case classes in my view is the copy method, which allows the user to change one or more arguments and have the rest automatically copied. That is more convenient and safer than just calling the constructor again with the new arguments because it ensures that all arguments that are to remain unchanged are accounted for properly. And that remains true even if arguments are added to the original constructor.

1 Like

I’m confused – how do case classes differ from conventional classes in this regard?

On the general point, I strongly agree with @sangamon. Case classes are absolutely central to modern, idiomatic Scala. They work fabulously for straightforward data types – immutable, with efficient copy constructors, and standard functions defined in standard ways based strictly on the constructor parameters.

But when you start coloring much outside those lines, you should switch to conventional classes. That doesn’t mean case classes are bad – it means they are intended for some (extremely common) use cases, but are not a one-size-fits-all tool. Conventional classes are the general; case classes are a specialization.

3 Likes