Make concrete classes final by default


#67

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.


#68

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


#69

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.


#71

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.


#72

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)


#73

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.


#74

I am much in favor of having final by default.

The migration effort would seem acceptable to me.


#75

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.


#76

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:


#77

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


#78

Have java interfaces the same problem?


#79

Java interfaces cannot have fields at all :wink:


#80

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:


#81

You can use traits like plain interfaces if you want.


#82

Not easily. The initialization order is all screwed up.


#83

Does that affect anything if no trait that is used contains any vals or vars, though?


#84

I do not know exactly.
But if take a look at trait decompilation:

trait SimpleTrait {
   var someVal : String 
}

javap.exe" -s -c -l  -p SimpleTrait.class > st.txt
Compiled from "SimpleTrait.scala"
public interface ru.bitec.app.gtk.lang.SimpleTrait {
  public abstract java.lang.String someVal();
    descriptor: ()Ljava/lang/String;

  public abstract void someVal_$eq(java.lang.String);
    descriptor: (Ljava/lang/String;)V
}

The fully abstract trait is a simple interface. So there should not be such problem with binary compatibility

So may be we need a keyword which guarantee that the trait has no compatibility problem.

It seems a bad decision in general.
Separating implementation from declaration should give much gain.


#85

I guess we disagree :wink:

That’s what they teach you in school. In practice, when you design a library, meant for long-lived binary compatibility requirements, a lot of design decisions are vastly different from what you do in applications with more-or-less source compat requirements.

See also my ScalaSphere talk, where I spend the entire talk on these issues:


#86

I guess you teach me more better :slight_smile:
The key word is “in general” :wink:

We implement our decision with such core libraries as:
-eclipselink
-jdbc
-anorm sql
And with any of that library the final classes or interfaces absence is a headache.
It is a real headache on practice.
We implement many services around this libraries and final classes force us to hack bytecode or use reflection and proxies

I recognize the words of a real teacher :slight_smile:

Ok you are right.
But in my practice we have to use aspectj to override the constructor to overcome this truth(or we should write diffrent branch of orm reader for examle.)
Of course someone can say you should change core library, ok we should.
But in real life our customers do not want to wait until the library author approve and merge this change.

So I love the teachers which let me get rid of aspectj :slight_smile:

And I love the libraries which can be decorated easily :wink:
For example I love XAResource(infinispan)

With that interface I can use atomics in small project. And customize it in ERP environment.

In java it is common practice to declare standard separately from its implementation(jdbc,jta and so on).
May be they were too good pupils. But I am really glad it :slight_smile:


#87

I do not argue that this approach is good for Scala.js

But if it is the general scala recommendation, It seems like language weakness.

I have never seen something like: “Do not use interfaces in java it cause compatibility problems”