Make concrete classes final by default

I would wish all concrete classes would be final by default in Scala 3.

Making a class “open” should require the new modifier keyword overridable (a nice symmetry with override) on the class or one of its members.

I think such a change could be handled by ScalaFix and would not need any manual interaction to preserve the programm’s semantics.

Concerning “repair / modification from the distance” of library classes / objects (traits?): It would be nice if Scala would get some AOP (aspect-oriented programming) support for that.

Only an opinion from some random guy on the internet…

1 Like

Is there more of an argument for making final the default than for making private the default?

If the writer of a library should have to be explicit about what parts of the library can be used in what way, doesn’t that extend to method access? Shouldn’t public APIs have to be marked as such explicitly?

Actually there is. See the interview posted by @alexandru. It is much more difficult to maintain and reason about the outgoing contract (which is only a concern for non-final) than the incoming contract (which applies to everything non-private).

1 Like

To answer all the question at once (out of my perspective): No, no, and no. :smile:

The messages you can send to an object are its public interface, aren’t they?

Methods correspond to messages so it’s quite natural that they are public by default. A non public method on the other hand is something special. It does not correspondent to a message. It’s an implementation detail! So it should be marked explicitly as such.

If classes wouldn’t be overridable by default there is even no strong need for hiding the methods anyway.

I strongly agree with those words. I feel it is far more practical to loosely declare which APIs are intended to be used and which are internals that should be used with care.

To continue this line of thought, I have to say that I also very much dislike when library authors restrict the scope of many of their classes to only be used by their library (private[mylibrary]). It has been too often that I just had to give up a desired feature or a bug fix in a big library (akka, scalikejdbc, etc) just because of restricted scopes. I’d rather have the authors mark those parts as internals (perhaps via annotation which would yield a compiler warning) than restrict them entirely.

I agree with a previous comment that it would be nice to have tooling support to crack open the implementation details of a class in order to apply a patch or test non-public implementation details.

These implementation details might be restricted members, or they might be local functions.

There are endless debates about whether it is wise to test non-public implementation details, where you would sprinkle asserts for invariants, and I don’t mean to reopen that side of the debate. But I would like to add here that local functions are not different from private methods with regard to testing and patching hacks.

2 Likes

It seems like there are two views on this – perhaps it’s largely people who value freedom to modify other people’s libraries vs. library authors who value freedom to modify their own libraries without breaking other people’s code…

(Personally I’m more in the former camp.)

In any case, it seems like a viable solution would have to satisfy both camps, and of course not break existing code.

Maybe we need three levels - (a) final for “if you extended this things would likely break, so just NO,” but also (b) “this is meant to be extended at will” (e.g., JComponent) and © “if you extend this, you’re taking your life into your own hands.” It seems reasonable to make the first two explicit, since they are the most opinionated. I think © pretty much describes how things work very often already. So we could say © is the default. On the other hand, adding a new keyword for (b) (like open or virtual) seems too big of a change, even if it weren’t a breaking change.

Two approaches:

  1. Add an annotation @open (or whatever bikeshed color) to document a class as an open hierarchy. A class with that annotation is in category ©, otherwise (without final) it’s in (b).

  2. In Dotty, traits can have parameters, and support for trait parameters may be coming even in Scala 2.x, which begs the question why have classes if traits can do everything classes do and aren’t restricted to single inheritance. So we say traits are (of course) in (b), and all (non-final) classes would be considered ©, and discouraged to extend.

In any case, we don’t want to be too harsh on people in the © camp. So extending something in © shouldn’t be an error, or even flood the build log with warnings by default. Instead, either collapse the warnings by default like is currently done with deprecation, feature, and unchecked warnings, or else make the warnings opt -in with -Xlint. If someone wants to kickstart it they can do it as a library / compiler plugin, or add support to wartremover or the like, and promote it wherever possible.

4 Likes

As you probably know, in Java, the default visibility is package privage which I personnaly think is the correct default if you think of your system as being assembled from components. public and private require explicit annotations which supposedly make them that slight bit harder to use therefore encouraging good design. Sadly the thoughtfulness put in choosing this default was totally defeated by IDEs making everything public by default in their templates and upon usage.

the interesting point about package private is that it is not impossible for someone with sufficient motivation to overcome the encapsulation from outside the library code base. One can create a class in the library’s package to access whatever one needs.

Could the same thing be applied to final ?

  • final : no one can touch this
  • package final: no one can extend/override this from outside the package (would be the default)
  • override: you are encouraged to extend/override this.

(override may not be the best way to express this but it has the strong advantage of not requiring the creation of a new keyword. adding keywords to a language is always quite painful for end users).

I don’t know the JVM/compiler well enough to know if this is achievable or not

This proposal (@nafg’s) is actually very sensible. I would endorse it.

I would just keep @open in dotty. There are still use cases for open concrete classes even when traits can get parameters. The most important ones are Java interop and their properties wrt binary compatibility.

Excuse my ignorance but what would be gained by this proposal? I don’t get it.

I read this as: Everything stays the same except there would be a new annotation that suppresses some warnings.

Did I misunderstood something? What problem is solved this way? Could you explain please?

The original proposal solves a real problem, and would make a best practice the default (also helping newcomers to the language doing “the right thing” without thinking about it). The coast of that change is also low. It would be a fully automatic rewrite. In some well written code-base it would also reduce the visual line noise by removing the “final” keyword that is used in almost every declaration. Such change would also make “open” (or “overridable” which I prefer due to symmetry but that’s a bikeshed color anyway) visually stand out, marking clearly the places some external implementation can hook in.

Removing classes from the language does seam problematic also. How would an code upgrade to scala 3 look like? Can really every class be simply rewritten to a trait? I’m not sure about that.

And like I said: Extending random alien classes not owned by yourself isn’t the clean approach to “bugfixing” / “experimenting with” library code in my opinion. AOP would be a much cleaner pattern to achieve the same goal.

2 Likes

No matter the exact keyword used for this, if this feature does get implemented with an open keyword or similar, it doesn’t have to be backwards incompatible wrt the keywordization, right? Couldn’t open (or overridable) be somewhat like Java 9’s new restricted keywords, i.e. normal identifier unless it appears in a context where an identifier couldn’t have been before in the first place (as we have @annotations unlike Ceylon).

Or is the implementation of that/technical debt too high for consideration? (As I have not much insight into the compiler’s design)

No, it adds warnings. The annoyance is that the “best practice” would require writing the annotation by default. I’d actually prefer having the annotation for (b).

We’ve chatted about it on the Dotty side, it looks plausible but we haven’t tried out how invasive it’d be, and it’s not been a priority yet.

AOP isn’t coming to Scala. AOP research has been mostly abandoned, because AOP is too powerful and fragile, especially if the advised code evolves without accounting for the aspects. Even its flagship conference (http://modularity.info/) died out / was renamed into something else.
We understand the tradeoffs of the OOP approach much better, even tho it’s not perfect, and we have some sensible guidelines (see Anders Hejlsberg’s interview); AOP has all the same problems (and more), but magnified.
I admit “final by default” would be cleaner, but even with ScalaFix the transition cost is too big, as ScalaFix doesn’t rewrite existing knowledge in either heads or docs.

I think that’s only sort-of supported, code using that will sometimes break, especially in libraries and breaks at least with OSGi; I don’t use it myself, but some libraries need to.

I am much in favor of having final by default.

The migration effort would seem acceptable to me.

There are really three desired states:

  1. The author supports / intends users to extend.
  2. The author does not support users that extend it, but does not forbid it.
  3. The author explicitly forbids extension.

#2 is probably the best default. In Java some libraries use an annotation to make it clear when it is case #2 and leave the language features for #1 and #3. Users that extend classes in case #2 are on their own if they are broken by later changes.

I do not think it is a good decision. We often make library fasade and final class is often headache.
We can make public trait and private class.
And nobody will have problem.

  • Trait cannot be overridden by mistake :slight_smile:
  • It is very easy to decorate trait

So library users will be happy, I will definitely be :slight_smile:

1 Like

Non-sealed traits cannot receive new fields in backward binary compatible ways, even private ones. They are usually not suitable for evolvable library design.

Have java interfaces the same problem?

Java interfaces cannot have fields at all :wink:

1 Like

we use java interfaces to separate the declaration from implemantation a lot in java. In scala we use traits for that perpose. It is sad that there are no simple way to do it in scala (((
Plain interfaces is a very good thing, may be the real problem is its absence? :wink:

You can use traits like plain interfaces if you want.