Make concrete classes final by default

I wonder if any good use case at all exists for concrete non-final case classes. Otherwise abstract case class already exists, and no new keyword is necessary.

There, Odersky told they are used: https://groups.google.com/d/msg/dotty-internals/uoQfrMQ4baE/gOSC9iCGDAAJ

Not to be defetist or anything, but Iā€™m asking for final case class (and just that) since ~2010. There is exactly 0 chance it happens in a non major language version, so Dotty is the best option for that.
There is also exactly 0% chance that it happens without a clear leader who want to go deep in the specification, have answer to all the portability/breaking change questions, hold the future SIP, fight for it against others, and well, do all the grunt work.

And even with that, there is very little chance it happens, Odersky not being in favor of it (in the past at least). But it was said at some point that Dotty may have a new way for ā€œAlleviating the syntactic burden of expressing ADTsā€ (with perhaps more final in the process): https://groups.google.com/d/msg/dotty-internals/uoQfrMQ4baE/gOSC9iCGDAAJ

I know Odersky uses case class inheritance, but Iā€™m not sure he actually inherits from concrete case classes. And with a quick look at the dotty codebase I only found some instances of inheritance of abstract case classes. I might be overlooking them of course.

A final on a case class should probably be the default. I actually can imagine this making it to the specification since as you say the deprecation is already there. What are your thoughts @odersky?

Scala 3 has enums, which translate into final case classes:

https://dotty.epfl.ch/docs/reference/enums/adts.html

2 Likes

If we make case classes final by default, we need a way to make them open (and no, abstract is not good enough). Overall, things get more complicated. open as a modifier would require we allow identifiers to be modifiers (since there is no way we can reserve it as a keyword now). That would make things even more complicated.

Since we now do have a way to make lightweight final case classes using enums I believe thereā€™s less need for this. If you want lightweight syntax you are well advised to use enums anyway.

In retrospect I also would have liked final to be the default (for all classes), but itā€™s both too late and not important enough to reverse this now, I feel.

5 Likes

For an example, look at dotty.tools.dotc.ast.Trees.scala

Thereā€™s an Ident case class, and BackquotedIdent subclass.

1 Like

Just out of curiosity, why would not abstract be enough?

1 Like

Just out of curiosity, why would not abstract be enough?

a) Itā€™s another irregularity.
b) For an abstract case class I have to create an apply method explicitly, and it has to
create an anonymous subclass. Also, I donā€™t get a copy method.

1 Like

a) Itā€™s another irregularity.

In what way, if I might ask? The reason I ask, is that it is already possible to design classes abstract without requiring them to have any abstract/unimplemented members.

b) For an abstract case class I have to create an apply method explicitly, and it has to
create an anonymous subclass. Also, I donā€™t get a copy method.

In the case of abstract case classes, FWIW that has that not been recommended against since at least as long as Iā€™ve been around? Speaking of the copy-method generation, this reminds me of this issue: copy method invisible to typer? Ā· Issue #5122 Ā· scala/bug Ā· GitHub

Couldnā€™t Scala switch to having final case classes by default and use a feature import/feature flag to disable this ? just like what has been done for postfix operator notation or implicit conversionsā€¦

Importing this where needed would then be a matter of updating scalafix to detect the cases and add the import in existing source files which need it.

2 Likes

Can I please extend your class? I swear I know what I am doing?

How about, instead of final, an annotation that emits a warning when some one tries to extend the class, and a mechanism to disable the warning? Case classes can be thusly annotated by default.

Like:

**@DangerousToExtend(because=ā€œThe implementation relies on weirdMethod doing crazy stuff.ā€) **

class DangerousClass {

def m: Unit = crazyStuff()

}

class NaiveExtension extends { // emits warning

def m: Unit = theWrongStuff()

}

@ExtendingThisWhileKnowingWhatImDoing(because=ā€œIā€™m sure it still works the way I reimplemented method m.ā€)

class SafeExtension extends MyDangerousClass { // no warning

override def m: Unit = equallyWeirdStuff

}

case class CaseClass(a: Int, b: String)

class NaiveCaseClassExtension(a: Int, b: String) extends CaseClass(a, b) // emits warning

@ExtendingThisWhileKnowingWhatImDoing(because=ā€œI verified it still works.ā€)

class SafeCaseClassExtension(a: Int, b: String) extends CaseClass(a, b) // no warning

Best, Oliver

I am against having one default for classes and another, even non-overridable one, for case classes. It just feels unsystematic to me.

It would be nice to have the ā€œclosedā€ default for all classes, but itā€™s too late for that.

Given these constraints, I believe the advice ā€œif you want lightweight final case classes, use enumsā€ is quite adequate, no?

2 Likes

In my opinion, having a ā€œmaybe extendableā€ is the worst case of both world:

  • you canā€™t get any of the (perf) optimisation final can give you (for the compiler and the jvm, ā€œmaybe extendableā€ is the same as ā€œopenā€)
  • you donā€™t get any maintenance benefits: you will break user code if you change that class, people will use them (proof: the number of cases of Thread.stop() or other deprecated Java methods since the night of times)
  • you put your user in a terrible choice (use something that is designed to break at some point, miss a featureā€¦)
  • you keep being lazy and donā€™t thing what are the real extension point of your lib/app, and what is the real contracts you put with your users.

So, it seems to be a case where you are going to pay all the price of the alternatives for no benefits.

1 Like

Why doesnā€™t that logic also apply to the implementation case classes of enums?

Good question.

  • Case classes are in a sense classes on steroids. They give you added capabilties compared to a normal class.
  • Enum cases, by contrast, are a special case of classes. They do not give you all the flexibility of classes and offer in return a concise and legible syntax. For instance an enum case is data only; it cannot have a body that defines methods. So it makes sense to enforce that property by making the case final.
1 Like

Given these constraints, I believe the advice ā€œ if you want lightweight final case classes, use enums ā€ is quite adequate, no?

Alas, having to introduce two names where one would suffice seems like a worse UX?

enum MyCaseClass[+T] {
  case RealMyCaseClass(x: T)
}

The other option would be to go the same route as ā€œval / varā€ and enforce putting ā€œabstract / finalā€ when defining classes, so that it always becomes an explicit choice whether you want it to be extendable or not.

But case classes typically are part of an ADT. The ADT is the enum, not the case class itself.

You can just write final case class

1 Like