SIP: Make classes `sealed` by default

That’s a reasonable use case, although it sort of depends on how you structure your code. I pretty much always use interface-plus-implementation instead of using mocks (and thus, I use classes but rarely subclass them), but there’s a definite matter of taste there.

I said:

You said:

So I gave you an example; that’s all.

I think you’re reading more into this than is there – I’m just responding to what you’re saying, without any deeper agenda. While I’m mildly favorable towards the proposal at hand, I’m not especially passionate about it either way. (And this has become entirely tangential, one of those times that I really wish this system had threading…)

3 Likes

Yes, I think that’s the real answer. A trait does not call the constructors of its parents. In particular, it does not specify linearization order or arguments to constructors.

class C(x: Int)
trait T(y: Int)

// valid:
class Mix extends C(0) with T(1)

// invalid:
class Mix extends C(0) with T // Parameterized trait T lacks argument list

// valid:
trait E extends C with T

// invalid:
trait D extends C(0) // trait D may not call constructor of class C
trait D extends T(1) // trait D may not call constructor of trait T

[scastie]

2 Likes

After contemplating a bit, it still seems questionable to me.

If you don’t want the library user to override your final method, why at all do you want to expose your final method in classes.
Excluding the fact here that you can override methods of super classes with final methods of your class, final methods seems almost the same as static methods and static methods don’t exist in scala.
Why not do the same with final methods an instead make them an extension - or companion method.

Idiomatically, class methods should be default methods, i.e. signatures with a default initializer which would imply you could redefine them at instantiation time which is unfortunately hard to express with limited type systems as it is the case for java when considering methods encapsulating the surrounding environment.

It seems to me the desire is to implement an OO hierarchy resembling that of julia, where you have abstract types which are inheritable and structs which cannot be inherited from but nonetheless allow to choose functions at instantiation time, because functions are data.

I have now implemented a proposal with much reduced scope in https://github.com/lampepfl/dotty/pull/7471. Discussions on implementation details are best done on this PR. Here’s the doc page for the revised proposal:

7 Likes

I didn’t see this in implemented in the PR with open, so I will reiterate my question. I think it will be very useful to define a class as open only within the confines of my library open[MyLib] class Foo.

I also think that in the spirit of this proposal, traits and abstract classes should not be open by default, but open within the scope of the package they are defined in.

2 Likes

The problem with sealed[mypackage] is that packages are open which basically defeats the purpose of sealed.

1 Like

It’s not sealed. See the PR, it’s now using the open keyword.

Haven’t that changed with Project Jigsaw? AFAIR single package can’t be defined in multiple modules at the same time. C# has internal keyword which restricts access to the module (“assembly” in .NET parlance) as class or method is defined in. Scala compiler doesn’t have knowledge about modules but it has knowledge about packages and packages can be sealed by means of modules, thus this mechanism as a whole can work.

1 Like

Fascinating. It never would have occurred to me that this works:

trait E extends C with T

So this is (at least sometimes) an alternative to self-types, the way I’ve often seen them used – a class can only extend E if it also explicitly extends C and T, with their parameters properly specified. Neat…

Yes, it can be viewed as publicly-visible self type. But it only works with trait and class types, whereas self types can be given any kind of type (such as a union), and it forbids cyclic extensions (which is why self-types are often used instead).

2 Likes

That new design is 100% the right design, IMO.

Can I also get a linting option that will report an error (or a warning) if I define any class that is neither open (including not abstract), sealed nor final? I would use it in all my libraries.

2 Likes

However, I think this is a bad habit…

I agree, I hate having to do it, and it’s always my last option.

So making a fork of the library…

That’s great if you have the source, but that’s not the world that all developers work in. When I ran into this in my C# days I mostly ran into this with the .NET standard library : (

Still, there is value in quick and dirty solutions to Get Things Done …

My point is more that sometimes the quick and dirty solutions are the only way to Get Things Done. My fear of open is that it could eliminate that option of last resort.

1 Like

+1 I’ve long wanted to write a library of macros that would let you access private members by making the macro emit the nasty reflection stuff. I’d probably name it something like scala-deal-with-the-devil or something to reflect how often anyone should be using it ; )

Another question: would that even work in the JVM? I know some places in the standard Java that they use sealed for security stuff, like ClassLoaders and such. Full disclosure: I’m out of my depth on this question.

1 Like

Note that this issue was significantly improved in Scala 2.12 and onwards because Scala 2.12 uses interfaces from Java 1.8 plus Zinc now uses name bashed hashing so the compilation would be slower, but not by that much.

There are of course other benefits of abstract class vs trait

Yes, but I am afraid this will not make the cut for 3.0 feature freeze. It’s new functionality that can still be added later.

Does this mean that since final is still an explicit modifier now, that there are still performance benefits when marking a class a final (as opposed to not marking it at all)? Because probably the bytecode cannot be optimised otherwise, as there is always the option to import adhocExtensions?

No final will be generated in the bytecode. But I don’t think it matters for performance. There could be scenarios where it matters for security.

1 Like

So in the most recent version of this proposal, has item number 4 been removed?

  1. A sealed, non-abstract class C (i.e. the default) is allowed to have abstract members. It is then checked that all non-abstract direct subclasses of C implement these members. This is easy, as all these classes are in the same compilation unit.

I didn’t see anything about this in the dotty-staging writeup. If not, I would prefer this not be included, for reasons pointed out by @Jasper-M. In particular, I imagine it would be confusing to non-advanced Scala users. I imagine someone asking me if there’s anything wrong with their code:

class Foo {
  def foo: Int
}

then saying “Oh, well this might be valid code, or might not be. Can’t say for sure just from that snippet. Is there any other class extending Foo in the same source file that implements foo concretely? If so, then this is valid, otherwise no it is not valid.”.

3 Likes

So in the most recent version of this proposal, has item number 4 been removed?

yes, this part is removed.

1 Like

Giant :+1: on this proposal.

The decision to allow the existence of “third-party” subclasses, with overrides of arbitrary methods, must be taken with careful consideration (does the logic actually support extension, or could leaks or other types of problems arise with the existence of subclasses?), and should therefore be intentional.

This is a very nice and welcome “guard rail” that keeps developers on the happy path, without requiring them to think in the default case; but which in no way limits flexibility for the power user who understands the full implications of implementation inheritance.

4 Likes