SIP public review: Open classes

I’m warming up to the idea of an @open annotation on methods intended for extension, or alternatively on whole classes if all their non-final methods are. The rest of the proposal would still hold as is.

2 Likes

This has already been discussed at length over two discussions in this forum; there were no clear conclusions other than the realization that the topic is heavily controversial, and so do this proposal and its motivation. The language should not include such a core feature – tied with its own keyword – if its so controversial. On the other hand, an @open annotation with an opt-in compiler warning? Sure, why not.

Furthermore, I find major flaws in the logic and wording of the motivation:

When writing a class, there are three possible expectations of extensibility:

  1. The class is intended to allow extensions.
  2. Extensions of the class are forbidden.
  3. There is no firm decision either way.

The three cases are clearly distinguished by using open for (1), final for (2) and no modifier for (3).

(1) is clearly distinguished from (3) by using either trait or abstract.

It is good practice to avoid ad-hoc extensions in a code base, since they tend to lead to fragile systems that are hard to evolve.

This is clearly an odd statement given that implicit is such a major feature in the language, used heavily to provide just that – ad-hoc extensions via type classes and extension methods.

I’m not really sold on open, so I don’t really have any skin in this, but this doesn’t really make sense to me as a counter argument.

Extension methods and typeclasses can only combine the various methods of a class’s external API and can’t be used to override methods, so I don’t think it’s valid to compare them with extending a class or overriding it’s methods.

2 Likes

I’m not sure whether it’s a counter-argument, a comment about the particular wording, or both.

“ad-hoc extensions” is not a term I’m familiar with, but I am familiar with ad-hoc polymorphism, which type classes are a form of.

While I agree that this mechanism can be useful in some scenarios, I’m concerned that it might:

  • subvert expectations
  • add unnecessary inconvenience

I believe that the following idiom is very common in many programming languages:

class A
class B extends A

and that the common expectation is that this has the same meaning independent of whether A and B are defined in the same file.

Introducing the open mechanism would break this expectation. In other words, a feature that might be useful in certain advanced usages of class hierarchies, would introduce a non-optional restriction that might confuse user coming from other languages, and inconvience users who have no need for the feature. This is in contrast to other modifiers such as protected and final which adds a restriction when present, not when absent.

Additionally, one should remember that programming is not an homogenous art - there is a wide variety of programming domains, and different programming techniques will be appropriate for different domains. As such I object to the following generalization (from the “Motivation” section):

The class is intended to allow extensions. This means one should expect a carefully worked out and documented extension contract for the class.

In a low-level library consumed by hundreds of users, yes, perhaps. But in simple scenarios like e.g. modules for code re-use and organizational purposes

class CommonInvoiceOps {
  // methods
}

object AbcInvoiceMgr extends CommonInvoiceOps {
  // more methods
}

object XyzInvoiceMgr extends CommonInvoiceOps {
  // more methods
}

I would certainly not expect a “carefully worked out and documented extension contract for the class”.

As each of the classes above grows larger, it might make sense, simply for maintability purposes, to move them to seperate files. Having to add open on the base class just because it is in a different file would imply a significance that is simply not there.

So, in my opinion:

At the least, the mechanism should work on the package level and not the file level.

(On a related note, I would really like sealed to be package-level as well. Actually, I think the concept of “file” should not have any semantic meaning in a program at all.)

I have long since stopped writing final on all my classes (and methods). I feel a programming language should make it easy for me to express my intentions. I grew tired of the ritual of adding final everywhere and interpreted this as the language doesn’t support me here, too bad. I rarely design for inheritance apart from ADTs.

Now that the open keyword is added to the language I would like to propose/request a compiler plugin/flag to allow me to set my compiler to all-closed/sealed/final, for my own source. This almost always corresponds to my intentions. If not, I can explicitly add open or sealed to it now :smiley:

This would be opt-in and I believe would allow a more organic growth towards this if this turns out to be the direction of the Scala ecosystem. It can be advocated like already has been done for sets of compiler flags. This approach might help resolve the discussions that sprouted from earlier proposals of default final/sealed.

6 Likes

Here are some notes I took for myself, as I wanted to explore the 3 capabilities in play:

  1. being able to see a class
  2. being able to construct a class
  3. being able to extend a class

(1) trumps the others: you can’t construct (2) or extend (3) a class unless you can see it.

And for the most part constructing a class (2) is a pre-requisite for extending it (3):

error: constructor C in class C cannot be accessed in class D
class C private; class D extends C
                                 ^

The exception to the rule is abstract which denies constructing (2) but without denying extending (3).

In terms of modifiers, visibility/access modifiers for “seeing” a class precede the class keyword, while modifiers for constructing the class precede the constructor (with a syntax that’s not well known).

The proposal for open is similar to the sealed feature, which is a modifier that precedes class and modifies (3) the ability to extend the class, by making it more unrestricted than the default.