PRE-SIP: Make classes `sealed` by default

I would rather focus effort on whole-program linters that can warn you “hey, this thing looks like it could be final/sealed/whatever”, and can make the change for you if you agree.

The problem with that is that it makes code either safe or verbose. It’s awkward to have to choose between the two.

Yes, please. Scoping sealed would allow me to pattern match my public classes exhaustively, without that guarantee leaking to my users.

2 Likes

Any change may have unforeseen unintended consequences that may need addressing.

As a consequence, the more Scala 3 will differ from Scala 2, the more Scala 3.1 will differ from 3.0.

2 Likes

Coming from kotlin(no scala for the moment at the office), I think it is a good thing to have classes final by default as in Kotlin. However, there might be a need for a plugin to automatically have classes open as in kotlin so as to play nice with frameworks like Spring(sorry for the mention of spring).

I’m very strongly against using this as justification to stuff more things into an already over-full release. That is a recipe for endless feature creep and never releasing. Do we want Dotty to land in 2020-2021, or 2025-2030? Typically breaking changes of this magnitude do spend O(years) before being enabled by default, for good reason, and have a migration period of another O(years).

Stuffing more breaking changes into Scala 3 both postpones the inevitable migration while simultaneously making it more difficult: this seems entirely the wrong approach when we should be facing these migration issues up-front and making sure the migration is as smooth and easy as possible. If we want the language to evolve, we need to figure out a way to bring the community and ecosystem along with it for each step, and not hope for some big “rewriting the books” event to solve all our migration issues while working to make the migration as breaking as possible.

If some things need to wait until Scala 4 released in 2025-2030, that seems better than having widespread adoption of Scala 3 (including in the community and large commercial codebases) being delayed until 2025-2030!

14 Likes

scoping sealed just like with private I believe is the right way, and will allow devs to manage their code, just like with private scoping. So the power remains in the hands of the developer.

1 Like
  1. From experience, I’ve learned it’s almost impossible for developers to predict how their classes will be extended. This makes open-defaults appealing.
  2. This is a huge departure from today’s default. It needs to be reasoned about carefully.
  3. Whether this change is the right thing to do is debatable at best. .NET Developers like @arkban are the best point of reference since they’ve seen this in large codebases.

Therefore, cramming this in at the 11th hour seems terribly, horribly, wrong.

5 Likes

@lihaoyi

We will go very soon in feature freeze. The only thing holding us back right now is that the design and implementation of quote/splice macros needs some more work to figure out whether it can completely replace inline matches or not. This will decide whether to include inline matches in the language. I help where I can on this already.

The pre SIP was motivated by a last sweep to decide what features must be considered before the freeze because they would be impossible to add at any point later. Changing class extensibility defaults falls into that category. No matter what people say, this will be impossible to change at any future version. Now is the last chance we can do it. And, yes, we might decide it’s too big a change (I used to argue that a year ago). In which case the case is closed for good.

The current proposal will not delay the date we can go into feature freeze. It essentially requires a well-reasoned decision, not lengthy implementation work.

Comparisons with .NET are misleading because what is proposed is different. The .NET discussion is about virtual/non-virtual methods, not about class extensibility. It would be more helpful to get reports from people using Kotlin which has a design closer to what is proposed here.

2 Likes

If this sip is implemented it means very simple things to me.

  • I will have to make more forks.
  • I will have to make more wraps around java instead of using scala libraries.

I will recieve nothing for increasing cost to create productive environment for my company. It will make migration to dotty more difficult.

It is good way to decrease binary incompatibility problem isn’t it…

1 Like

I agree that it’s sometimes useful to extend a library class and override some of the methods in ways that were not foreseen by the library author. I’ve done it myself several times.

However, I think this is a bad habit, because it makes your code tightly coupled with code you have no control over, which may change in breaking ways in future versions.

So making a fork of the library, or even copying the offending class into your own project (under a different package), if it’s not too big, seem like better approaches. It means you now have full control on how that part of the code evolves, and you can freely adapt it to your needs. The latter approach works best if the library is organized in a modular way, which is hopefully the case of the average Scala library.

Still, there is value in quick and dirty solutions to Get Things Done, as long as it’s blatantly clear that it’s a temporary hack (so it could be rejected easily from clean code bases). I’d like to have an annotation named @disregardSourceRestrictions that allows one to extend final and sealed classes or methods and to access private members, perhaps only enabled under a compiler flag.

6 Likes

Not in my case. I like to extend production classes in test code. I have full control over both (production class and test one). OTOH I can add open or whatever in that case, but I would rather want the option to enable openess wholesale in e.g. whole test code.

I have to STRONGLY disagree. Effort of spinning up a repo, jenkins job for publishing and then keeping fork in sync is so huge I almost never do it. For me its either monkey-patching or not using the library at all.

5 Likes

Prioritization issues aside, my opinion of the current proposal is that it’s probably currently too contentious to be pushed into the language in any short timescale. Clearly lots of people enjoy the ability to unilaterally “patch” classes they use via subclasses, even if only in narrow circumstances like tweaking misbehaving third-party libraries or injecting hooks into test code.

For myself personally, my work codebase does this a reasonable amount, even if we generally try to avoid it. Generally if we can’t “patch” things via inheritance, we’re forced to mangle source code and maintain patch files or branches, both of which are a huge pain. We generally don’t inherit/override concrete classes much in “normal” circumstances, so the fact that you can doesn’t really cause us any hardship or mess

What about if we were less ambitious: we only make case classes sealed/final by default, and we make the sealed modifier transitive to automatically apply to subclasses? That would fix one long-standing wart while also likely not cause any migration pain, since people generally treat case classes and sealed hierarchies as final anyway (even if they forget to seal everything as necessary to enforce that)

10 Likes

@LPTK

Still, there is value in quick and dirty solutions to Get Things Done, as long as it’s blatantly clear that it’s a temporary hack (so it could be rejected easily from clean code bases). I’d like to have an annotation named @disregardSourceRestrictions that allows one to extend final and sealed classes or methods and to access private members, perhaps only enabled under a compiler flag.

I like the general idea. In the previous thread Make concrete classes final by default, it was stated that one can distinguish three possible states when writing a class.

  1. You do intend that the class can be extended. This means you have to carefully work out the internal contract for each overridable method. You communicate this by making the class open.

  2. You forbid that the class is extended. You communicate this by making the class final.

  3. You have not made a firm decision. The class is not a priori intended for extensions, but if others find it useful to extend, let them go ahead. However, they are on their own in this case. There is no documented internal contract, and future versions of the class might break the extensions (by rearranging internal call patterns, for instance).

Right now, (1) and (3) are not distinguished. The current SIP proposal could change this by using open for (1), final for (2) and no modifier for (3).

Now, the question is, what should happen on the user side if somebody does extend a class of the third category? There should be some form of indication that this is a risky operation from the standpoint of software evolution. It strikes me that we could use a language import for that. E.g.

import language.adhocExtensions

class C extends otherPackage.D

This achieves several things:

  • Library users can still write ad-hoc extensions, like they do today, but they have to opt-in with the
    language import.
  • Mocking works without warnings. Just set the language import in your mocks.
  • A simple grep checks whether a codebase is “clean”, i.e. that it does not use ad-hoc extensions.
  • Library writers are protected: If they don’t make a class open, no internal contract is assumed and they are free to change implementations.
  • Migration headaches go away. If a library chooses to keep a class C non-open then code extending C has to add a language import. That’s a simple user-side operation. No coordination between different code bases is needed. If C is actually intended to allow extensions, the library writers will be lobbied to make it open, which would also be a good opportunity to make sure the internal contract is well documented.

So, it looks to me we have something which would be very easy to implement and would likely produce a helpful social dynamic for the interactions of library writers and users. The scheme also follows Scala’s philosophy to always provide an escape-hatch for the cases where static checking is too restrictive.

With this revised proposal, what is the role of sealed? sealed is still needed because it makes it an error to extend a class in another compilation unit instead of just requiring a language annotation to do it. But as far as pattern matching is concerned, default classes should be treated like sealed classes: we know all their planned extensions so we can do exhaustivity analysis based on this.

8 Likes

sealed can be extended as proposed here: Pre-SIP: sealed enumerating allowed sub-types

If you never extend a normal class than you probably shouldn’t be using a normal class. case class definitely makes sense to not be extended, but a normal class being used in the Java OOP sense are designed in such a way that being open as a default makes sense.

In general I don’t think this is a great idea, the only data structures that should be non extensible by default are case class's, because it really doesn’t make any sense to make the data structure open.

Its also too big of a change for Scala3, Scala3 already having enough changes.

Mm that doesn’t make much sense to me. A case class is like an OOP record with equals, hashcode, copy, etc. If a class doesn’t need those there is also no need to make it a case class. I use classes for ‘services’, not for data, and rarely need to extend a service with more functionality, so they are almost always final.

If we take the time to do this it shouldn’t be that disruptive. If you first deprecate the current behavior before disabling it.

2 Likes

I agree with the proposed change, but I am not sure I agree with this line of thinking. Scala 3 will not be perfect and not the last breaking change of Scala. And if it will, it is likely to stay behind. Breaking code is on the long term inevitable if you want to get rid of warts / not so great design decisions. As long as there is a clear migration path & enough time to migrate it should be fine.

So even if this preSIP will not make the Scala 3 cut, I think it would be quite a loss to not consider it ever again.

1 Like

It seems that’s precisely the kind of situations where you’d be advised to use the escape hatch that I proposed — or the proposed adhocExtensions — which you would enable for the whole test project.

1 Like

And thats the point, I also use classes for clients to services (I assume thats what you mean), but in this case I find myself having to extend such clients on many occasions (i.e. mocks is a classic example, but also when testing corner cases)