Make concrete classes final by default

Extending concrete classes is permitted in Scala, unless the class is marked by a final modifier, however actually extending concrete classes is pretty rare in Scala, and is widely regarded as a code smell in most styles of programming in Scala. I think that in keeping with Scala’s philosophy of nudging programmers towards better practice by default, it’s a good idea to make concrete classes final by default unless opted out of. I think that this will be a very non-controversial issue as well, since both OOP and FP camps both consider concrete inheritance a smell (at the least). I’ve seen talks about FP in scala for instance where every case class is labelled final.

For inspiration you can look at what Kotlin has done. In that language, all concrete classes are final unless marked open. It would look like this:

open class A
class B extends A
class C extends B // compile error

Likewise, one could go even further, as Kotlin has, and make concrete methods final unless also declared open.

trait A {
  def x: Int
  def y: Int = 1
  open def z: Int = 2
}

class B extends A {
  def x = 0  // ok
  override def y = 2 // NOT OK 
  override def z = 5 // OK 
}
11 Likes

I’m am quite in favour of making things final by default (also private, for that matter). However I have a feeling this would break a LOT of code (though it seems like a trivial rewrite to retain current semantics). I’m not sure about methods being final by default, but certainly classes. There are a LOT of bugs that would be fixed by that (e.g. :: being extendable in 2.12)

For reference, Rust also follows this philosophy.

1 Like

@NthPortal just to clarify, here you’re talking about private-by-default right? There’s no such thing as overrides in Rust is there?

I just mean the maximum-safety-by-default philosophy. This includes private by default, immutable by default, and probably other things I haven’t thought of. You are correct that Rust (at least AFAIK) does not have inheritance or overriding

In addition to my :+1: (I mean :heart:), to continue to brainstorming here, I assume adding abstract or sealed would implicitly make a class open.

1 Like

You are correct in the case of abstract, this proposal is only about finalizing concrete classes, and abstract classes are not concrete.

However in the case of sealed, I don’t think this necessarily goes without saying. Almost all uses of sealed today are on sealed trait and sealed abstract class which would remain open to extension, but you can also have a sealed class which is concrete. I guess it’s up for debate whether to make sealed concrete classes open by default, but it could easily go the other way too.

2 Likes

In my opinion, everything should be final by default: classes, traits, def members, val members, type members.

I don’t think it makes much sense to make traits final by default – their whole point is to be extended/implemented.

6 Likes

A question to more experienced fellows here: isn’t it restricting extensibility? Right now if there is a bug or other unwanted behaviour in a class exposed from library anyone can override the behaviour and continue using it immediately even if author has not anticipated such use case.

5 Likes

I mostly agree with the argument that extending a concrete class is a bad-practice in most cases; however, I still oppose to making this the default behavior for the following reasons:

  1. This would probably break A LOT of code (mentioned by @NthPortal as well).
  2. Would harshly restrict extensibility of external libraries (mentioned by @Krever as well). In my experience I’ve come to override plenty of external concrete classes for the sake of better usability / bug fixes.
  3. This is the opposite of how Java behaves, which I fear would lead to quite a lot confusion for newcomers, especially in mixed Java/Scala projects.
3 Likes

Well, if we view the ability to patch external code by extending a class as a feature, perhaps we should remove final from the language. Cause right now the only reason you can do that kind of patching is because the author forgot to make his class final. Otherwise if it’s his intention that clients can do this he would make his class open in this new scheme.

That said, completely inverting open-final and/or private-public semantics is a huge change I’m not sure you can pull off after a language has existed for 15 years

5 Likes

If we were starting from scratch, I, and it seems like a lot of others here, would definitely agree with this. However, I’d like to echo the sentiment that this is too significant a breaking change to introduce at this point.

To provide a concrete example of why this is a problem, Mockito cannot mock final classes by default. It can be configured in recent versions by adding a configuration file to the classpath, but even then there are limitations when it comes to certain settings and alternative JVM implementations. See http://static.javadoc.io/org.mockito/mockito-core/2.19.1/org/mockito/Mockito.html#39

(Yes, I realize that there are problems with using Mockito, especially if you’re trying to use it in a functional context, but the fact is a lot of real-world code does use it. I know that my company would definitely be impacted heavily by this change.)

I would actually argue that Mockito is the way to go about mocking in Scala as well :wink: I had too many problems with ScalaMock.

I absolutely support the idea that everything non-abstract should be final by default, because 90% of the time this is programmer’s intention. However, something as fundamental as this is probably impossible to change for Scala now. Kotlin did it right by introducing open at the very inception of the language.

I can see some argument for doing this at the birth of the language, but now… not a chance!

The cost/benefit simply doesn’t justify it.

It’s impossible to know when/how another developer might need to use/extend your class. Yes… there are certain places where it’s very high-risk, and we have the final keyword for that. There’s also an argument for encouraging composition over inheritance, but more than once I’ve been in a place where it turned out that I could have easily solved a problem with access to some protected member and cursed whoever made the class final.

Compared to such weak benefits, the cost of potentially breaking a lot of existing code is very hard to justify.

There’s also no performance argument to be had here, as the JVM is already very good at identifying “effectively final” classes for optimisation reasons.

For case classes though? There’s much stronger justification for making them final by default, but that’s the sort of change that can’t be made until Scala 3, and even then the potential cost of introducing a new keyword to override this behaviour must be weighed up very carefully indeed. After all… open is used in a staggering amount of IO and network libraries!

I think this is the wrong choice because it is hard to avoid letting implementation details leak into your abstraction. Leaking details without actually providing the ability to manipulate or alter the details is incredibly frustrating. If you’re going to go to the effort to think carefully enough to hide the details appropriately, you can throw a final here and there on your internal methods to keep frozen what needs to be kept frozen.

If everything is immutable anyway, it’s less of an issue, but the cost of having something overridable is pretty low. Occasionally someone without much experience gets confused about how objects are remade, overrides something, and wonders where it went. Lesson learned, no more problem.

(Rust is different because there’s no inheritance, so final doesn’t apply!)

It seems like a combination of bad practice and solving a problem that doesn’t exist to make this change.

Not to mention that it’s a massive breaking change that is completely infeasible, but even if it weren’t, I think it would be the wrong thing to do.

On case classes, since overriding is basically deprecated anyway, I agree that requiring the final is mostly irritating with no benefit. That could perhaps be changed someday.

5 Likes

I’m not sure this change is worth the pain, but we could avoid breaking code if it was done through a long enough cycle, e.g:

  • In Scala N introduce open class to mean the same thing as class, also introduce a warning if class is used by itself asking the user to rewrite it as open class or final class
  • In Scala N+X, make it an error to write class without prefixing it by open or final, unless the flag -Xsource:N+X+1 is used, in which case class now means final class.
  • In Scala N+X+1, reintroduce class to mean the same thing as final class by default.
2 Likes

Haha yes, you’re right. A little bit over zealous without properly thinking there. But you know what I mean I hope :slight_smile:

The cost is actually pretty big, when you consider the evolution of a library over time while preserving backward binary compatibility (as we always should do). As a library maintainer, I have more freedom in the changes I apply to final classes than to non-final ones. For that reason, we have a design guideline in Scala.js that everything must be final or sealed, unless explicitly intended to be extended.

Oh and because someone is bound to answer my comment with something like “one more reason to do source dependencies instead”, please don’t: maintaining backward source compatibility in a non-final class is much much much harder than maintaining binary compat.

7 Likes

We could do that, but only if there’s a sufficiently good enough reason for choosing to make it the default behaviour in the first place.

I don’t think that’s yet been demonstrated, and “being more like Kotlin” is not a sound design strategy… If anything, the language contains a number of questionable anti-patterns that we should seek to actively avoid.