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:
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.
For an example, look at dotty.tools.dotc.ast.Trees.scala
Thereās an Ident
case class, and BackquotedIdent
subclass.
Just out of curiosity, why would not abstract
be enough?
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.
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.
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?
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.
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.
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