Simple, Naïve, and Wrong: More than you wanted to know about Case Classes

No because amount the legacy code that abuse the broken implementation of equals is the compiler itself :man_shrugging:

1 Like

Then perhaps a Scala 3 compiler option is in order to prevent inheritance from a case class. I think that would be preferable to having everyone use “final case class” everywhere. One of the appeals of Scala is the absence of such code litter.

My situation is certainly not typical, but I have no intention of putting “final” in front of my case classes. I will just avoid inheriting from them – which I don’t do anyway. I’ll keep my “require” checks as they are as well. The minor benefits of the methods suggested in this article are just not worth the extra boilerplate for my purposes.

As for not throwing exceptions for “expected” errors, I have issues with that too. If you end up trying to take the square root of a negative number, then something is seriously wrong that needs to be corrected before you execute your code for real. If the problem is result of bad user input, then do your check on the user input. For me, the purpose of throwing an exception is just to alert me that a bug needs to be fixed.

4 Likes

I don’t think there’s anything unusual about that. I’ve been doing Scala full-time for nearly ten years at a bunch of companies, and I never bother putting final in front of case classes. Yes, that is theoretically a problem, but in practice I don’t believe it has ever, even once, caused an issue in my experience.

Folks make a mountain out of this particular molehill. Simply treating case classes as final works just fine in practice – demanding that they be explicitly marked as such is, IMO, a discipline that just adds boilerplate without sufficient value…

3 Likes

There was already so much change introduced in Scala 3, the resulting complexity had already gone extremely high. So, I’m pretty sure even if it had been brought up, it wouldn’t have made the cut as it would have just made the possibility of “unexpected consequences” of changed/new feature interactions so much higher.

Perhaps it can be pushed into a future Scala 3.x release.

Tysvm for your feedback. There’s a fairly active thread on Reddit productively critiquing this which you might also find useful.

1 Like

In Scala 3, you declare a class open for OOP-style extension (as opposed to extension).

➜  scalac -d /tmp mycase.scala badkext.scala
➜  scalac -source future -d /tmp mycase.scala badkext.scala
there were 1 feature warning(s); re-run with -feature for details
➜  scalac -feature -source future -d /tmp mycase.scala badkext.scala
-- Feature Warning: badkext.scala:2:23 ----------------------------------------------------------------------------------------------------------
2 |class SpecialK extends K(27)
  |                       ^^^^^
  |                       Unless class K is declared 'open', its extension in a separate file should be enabled
  |                       by adding the import clause 'import scala.language.adhocExtensions'
  |                       or by setting the compiler option -language:adhocExtensions.
  |                       See the Scala docs for value scala.language.adhocExtensions for a discussion
  |                       why the feature should be explicitly enabled.
1 warning found
➜  cat mycase.scala badkext.scala

case class K(k: Int)

class SpecialK extends K(27)

Enforcement looks like

➜  scalac -Werror -feature -source future -d /tmp mycase.scala badkext.scala
-- Error: badkext.scala:2:23 --------------------------------------------------------------------------------------------------------------------
2 |class SpecialK extends K(27)
  |                       ^^^^^
  |                       Unless class K is declared 'open', its extension in a separate file should be enabled
  |                       by adding the import clause 'import scala.language.adhocExtensions'
  |                       or by setting the compiler option -language:adhocExtensions.
  |                       See the Scala docs for value scala.language.adhocExtensions for a discussion
  |                       why the feature should be explicitly enabled.
1 error found

The new support for such extensions looks like

➜  cat mycase.scala
open case class K(k: Int)

or

➜  cat badkext.scala
import scala.language.adhocExtensions
class SpecialK extends K(27)

It’s worth adding that a FAQ for someone (possibly a learner or with shallow knowledge of the language) who just wants to use a feature might look different from an expert document that might represent a project’s code policies.

1 Like

If inheriting from a case class is really such a terrible idea, then yes, I think it should be prohibited in a future Scala 3.x release.

Is there any valid use case for extending a case class by inheritance? Just wondering.

1 Like

Awesome! Tysvm for elaborating!

When I get a chance (tomorrow?), I will add something about this, and a link to your response here.

Agreed.

And given how opinionated Scala 3 is over Scala 2 (by intention and design), I expect changes to pain points like this will be slowly incorporated.

You can see from som-snytt’s response above one way they have already started approaching this boilerplate irritant.

As far as I know there is exactly one person who does not agree and that person is Martin Odersky. Hence why the compiler still allows it.

1 Like

This was Odersky’s thoughts about case class as final as of 2019/Oct.

I read that to mean he thinks it is the better pattern. But, it is a language wart we must live with. To me, that means he might be open to a pathway that figures out how to solve the problem.

1 Like

FWIW, no surprises here, I am not seeing any mention of the need to make case classes final in 4.7 Pattern Matching - YouTube and in the modern version of the course: https://www.coursera.org/learn/effective-scala

1 Like

FWIW, in Scala from Scratch - Exploration, final is used and recommended straight away when case classes are first introduced:

Case classes are special boilerplate-free classes. If you define a case class, the Scala compiler generates a lot of the things a well-designed immutable data class is supposed to have for you — equals and hashCode implementations are only two of them. Let’s make our Color class a case class and then walk through the benefits we get for free. In the Color.scala, change the Color class so that it looks like this:

final case class Color(red: Int, green: Int, blue: Int) { 
  require(red >= 0 && red < 256)
  require(green >= 0 && green < 256)
  require(blue >= 0 && blue < 256) 
}

We define a case class by prefixing the class keyword with the case keyword. It’s highly recommended to make case classes final as well, because they are not actually designed for inheritance. This is why we prefix our case class definition with the final keyword as well.

1 Like

Related: the Java version of a case class, i.e. a record class, is implicitly final: " A record class is implicitly final , so you cannot explicitly extend a record class".

1 Like

Sure, but ultimately this is a matter of opinion. I mean, “highly recommended”? Yes, there are some people who recommend it, and also lots (AFAICT rather more) who just don’t bother. This is the author pushing their preference and making it sound like it’s more universal than it is. It’s the overstatements that bug me – saying that it’s worth considering I’d be fine with, but less with making it sound like the settled pattern. It’s a style guide question to discuss within a given codebase.

I don’t know of anyone who disagrees that case classes should be treated as final in more or less all situations – extending them and getting it right is hard, so that’s generally considered an anti-pattern. The disagreement is purely about whether it is worth the boilerplate of explicitly saying so all the time (IMO generally not), or worth changing the language to enforce it automatically (IMO yes, but apparently there is some disagreement there).

2 Likes

That is very well said.

Basically, this is an area where Scala (2.0) hasn’t been as opinionated. It is an opportunity for Scala 3.0 to become more opinionated in a future version.

1 Like

Here in “Scala best practices I wish someone’d told me about” is how Nicolas Rinaudo describes the problem with subclassing a case class and recommends using final (he even mentions Martin Odersky) Scala best practices I wish someone'd told me about - Nicolas Rinaudo - YouTube.

Corresponding slide Scala Best Practices I Wish Someone'd Told Me About

1 Like

The disagreement is purely about whether it is worth the boilerplate of explicitly saying so all the time (IMO generally not)

There are two possible answers to that.

The one I like to make, because I’m not a very nice person, is: agreed! this is also why I do not like putting types on my values. It’s not worth the boilerplate when most people know to use the right type anyway.

The better answer is: absolutely. I’m firmly in the camp of those that do not trust themselves not to make a mistake they know to avoid. I’m sometimes tired, sometimes drunk, and sometimes not even the person making editing my own code. If I can get an automated tool, such as the compiler, to do my checking for me, then so much the better! So I configured my compiler to fail on non-final case classes and never looked back. I disagree with, but respect, the notion that a little bit of boilerplate is too expensive a price to pay for getting rid of a potential tricky bug.

2 Likes

I couldn’t agree more.

In my own Scala experience, the software engineering dev environment is very different than the devopssec environment. And it appears that many make some pretty speculative assumptions about enterprise IT deployment environments.

I outline my own experiences with this more specifically within this response on Reddit:
https://www.reddit.com/r/scala/comments/t1hfzt/comment/hzgh8kc/?utm_source=share&utm_medium=web2x&context=3

While there is no such compiler switch built in, the wartremover compiler plugin provides such a check, see Wartremover : Built-in Warts

2 Likes