SIP: Make classes `sealed` by default

I guess we differ in style then, because we use classes the same, but not the way to test them.

I still think it makes sense to be explicit when something is meant to be extended, and I would like to default to the one with the least amount of power, thus final (or sealed) instead of open.

The suggestion LPTK makes is probably best of both words. You default to the one with the least amount of power, but still have the ability to break free from that convention for whatever reason you deem necessary.

2 Likes

I guess my point is, the whole point of classes is the open extensibility paradigm that comes with OOP, and they are primarily an OOP mechanism. Defaults should convery the intent of how the abstraction is meant to be used and the paradigm that comes with it, and when you use class the way its meant to be used you actually do find yourself needing to extend them fairly often (this is also a side effect that classes are often used to couple state with data, and since the state is coupled with data, inheritence is the primary way of extending functionality).

tl;dr is, if you are using classes the way they are designed to be used (in the Java OOP sense) then extended this is intended and should be classified as default behaviour. This is even more important if you write libraries (as opposed to applications) where the number of instance that you need to extend a class increase even more (as others have commented the only way around this is to manage manual forks)

1 Like

I guess we have to agree to disagree. Yes, extending classes is very OOP in Java. But I would not say it is recommended in Scala. OOP in Scala is much more about using objects as modules, not as containers of mutable state that can be extended indefinitely. At least, that is how I see it used often.

Patching a library should be quite rare I would say. And even if it is necessary, it makes more sense to use an escape hatch rather than have it be the default behavior available to you.

3 Likes

Oh of course, but thats why Scala has different abstractions for this. For example I strongly believe that case class should be final (and we can go even further if possible), but if you are using class’s you are already implying that you are coding in the Java/OOP style.

At least in non trivial applications, its definitely not rare (from my experience).

That’s just plain not true – you really should stop casually asserting it. Java/OOP style is fundamentally mutable in nature, and there are plenty of us who use classes for other purposes…

7 Likes

I have worked on Kotlin where classes are final by default. If you are going to mock your classes, you end up opening them all.

2 Likes

Inheritance is not particularly unsafe when applied to a carefully-designed API that is intended for it. (E.g. pretty much anything that supplies a default implementation for a visitor pattern.)

But I agree with your characterization for the (seemingly?) most common style used in Scala.

Note that lack of a MyType poses a considerably larger safety (and usability) barrier for the most common style than does allowing open inheritance by default. Dropping sealed here and there really doesn’t impede reading very much. Dropping [A <: Foo[A]] { self: A => all over the place is a lot more awkward, a lot harder to understand for novice/intermediate users (and it isn’t even perfectly safe). People don’t extend immutable stuff because it doesn’t work anyway–you learn this pretty fast, though, yeah, it would be nicer as a new user if you didn’t have to (and nicer as a library author if it was just taken care of, not something you had to do explicitly).

So I sort of technically agree, but I don’t think on the scale of things it’s worth the likely disruption. And I think the disruption would be substantial: library X recompiles successfully for the new version, changing something from innocuously inheritable to not inheritable, and some fraction of downstream users’ code ends up completely broken, requiring a major investment of manpower to fix (fork library, or submit PRs and discussion, at which point there are probably bincompat issues as well, or refactor code to (try to) work around the issue).

2 Likes

This saves a developer from writing one word. From the other side it makes class’s properties more difficult to track.

2 Likes

Such as (honestly I would like to see some examples)?

  • If you need tuples, Scala already has syntax for this (i.e. Tuple/())
  • If you need to carry around data, i.e. “data classes”, you have case class
  • If you need singletons, you have Object or case object
  • If you need interfaces/mixins then you have trait

implicit class’s are the only other thing that I can think of, but these are really just extension methods and they are being redone in Scala 3 anyways.

Note that I am not saying that there aren’t cases in Scala that you want a final class, what I am saying is that unlike Java where you have to use class for almost everything, Scala has different abstractions. Of course nothing is stopping you from class for everything like Java, but then this is just using Scala as a better Java (and I would argue this isn’t idiomatic nor should we be encouraging it).

Also I don’t know why the tone is being so aggressive here, we are talking about sane defaults here and one of the points of defaults is to signal users how the abstraction is meant to be idiomatically used. Of course there will always be exceptions, but I believe there are much stronger arguments to make case class final by default since there is almost no reason why you would want to extend a case class (where as there are plenty of reasons why you want to extend a class if you want to use them as an intended class)

Precisely my point, we actually often did end up doing final class by default and then invariably we ended up having to remove final because extending them is a very common case.

If you want static singletons, that works. But the singleton pattern is a terrible way to architect serious programs, and most forms of DI / resource-discovery rely on instantiating a class at runtime. That’s nothing particularly Java-ish – that’s a common way of assembling a program from a collection of services, and I’ve used the same general pattern in at least six different programming languages, including a whole bunch of different DI libraries (including relatively FP-oriented ones like Macwire) in Scala.

And yes, you could technically do that with case classes, but IMO that’s an abuse of the case class mechanism, which is really intended for data structures, not top-level system services.

This is a strawman – I don’t see anybody above saying that you should just code with classes. On the contrary, you seem to be saying that folks should never use class (or at least, that using it automatically means “Java-style” code), which seems rather extreme. Most of us view class as a tool to be used appropriately, but not the be-all and end-all.

Um – you’re the one throwing out untrue claims about peoples’ programming styles. Are you surprised that they are unhappy about that? Seriously, you seem to be the one coming across as aggressive…

4 Likes

Yes, and we are using class to do this and we often end up having to extended such classes in our tests due to mocking.

i.e. if you have something like

class ItemClient(httpClient: HttpClient, database: Database)

In many cases due to testing specific situations, i.e. using an in memory database we often end up having to extend ItemClient to override the implementations in the methods of ItemClient to what you need to test. I am not sure why you are bringing DI into this anyways, because DI is about how you instantiate your “clients”, its not about extensibility, this isn’t really a concrete example.

You are twisting what I said, I said that classes in the way they are implemented (because Scala classes pretty much exactly mirror the traits of a Java class) heavily imply the open extensibility principle. There is a reason why, case class for example has an automatic implementation of hashCode/apply/unapply/equals/copy, because if you are using case class as intended you actually need these things.

Note that is nothing derogatory about Java OOP style if thats what you are implying.

No I am not, I am saying that the way classes are implemented (which is fact) and the origins behind their design (which is partly subjective and also my opinion) lends itself to being openly extensible.

@odersky Originally stated in his goal that he wants to make classes final by default because it brings forward antipatterns with inheritance if you leave them open. There are 2 paradigms where inheritence is classed as an anti-pattern

  • Mixin/trait style composition (i.e. Go)
  • Pure Functional programming with polymorphism (i.e. Haskell)

In either of these languages, classes don’t even exist! And in both of those cases, if you want to do these things in Scala you have constructs for doing so (i.e. trait and case class/case object). If you have a look at Scalaz/Cats (at least for the pure FP paradigm), classes are almost unused apart from exception cases (performance/MIMA/backwards compatibility/cross compiling/java interopt).

Even OCaml, which heavily inspires Scala and is probably the language closest to Scala in terms of design has classes which are open by default for inheritance, again because most OCaml programmers only use classes when in the cases where this makes sense (OCaml programmers also tend to ignore classes unless they really need them).

tl;dr Is, if you are using classes and you want them to never be extended (i.e. non final) there are usually (but not always!) better Scala abstractions to do this, and this is what we should be encouriging. There are also real concrete examples of languages making class’s final and the pain and annoyance this has caused (i.e. .NET). Most concerningly it forces library authors (if they choose to legitimately use class) to be precisely perfect in predicting how all consumers will use their library, or they will just spam the “open” keyword everywhere which then means its the wrong default.

Also at least if you are on the JVM, there is no performance penalty to having a non final class if no one extends it, JVM does class heirarchy analysis which means non extended classes become effectively final at runtime if nothing extends it during the running of the program.

1 Like

This is the problem I have with the argument that the default should be open because you need to be able to patch other people’s code. It is basically saying that this default enables you to patch other people’s code simply because most people are too lazy or don’t care enough to type final or sealed. But as soon as a person does care enough, or uses things like enums, you can no longer patch their code anyway.

If the default were sealed but with an escape hatch to use at your own peril, you would depend even less on choices made in upstream libraries because if really necessary you always have the escape hatch. And you could more easily use mocks during testing without necessarily having to open up your API for everyone.

8 Likes

To be fair, I am saying that the default for class’s should be open, not just generic code. If you explicitely dont wan’t things to be extended, there are Scala constructs for this and usually this is not a basic class.

I mean in the case of enums, this is often a deliberate decision by the write about whether its extensible or not. Even tools like swagger/OpenAPI have annotations/markers about whether you can add things onto an enum or not. The point here is though, an enum is not the exact same thing as a class. Its actually an ADT with generated methods for looking up a product type by its String name, and Dotty now has explicit syntax for this.

The point for leaving this open (specific to class here, not case class which should be final by default) is the library author is saying that “I am unsure at this point in time if this will be extended or not, but there is a case for it”. However if he is 100% sure, then he can make it final. The converse of this is very painful because then you either rely on

  • The library author having to manually make everything non final
  • Maintain upstream forks

The latter is a pain that we also have to deal with (either that or using reflection which is even uglier).

I don’t see how this is any different compared to just having class as open initially. If you make all class’s final and make some annotation based escape hatch, then people will just pollute their code with annotations which pretty much defeats the purpose of the default in the first place.

1 Like

I like this proposed change, as it coincides with my practice of having almost no use case of a class that is not either abstract or final. Makes a lot of sense to me.

I still think one could collapse trait and abstract class into one and the same thing, and eliminate the overlap.

2 Likes

I still think one could collapse trait and abstract class into one and the same thing, and eliminate the overlap.

The JVM has the restriction that a class can extend only one class, but arbitrarily many interfaces. How would that work if abstract classes and traits are the same?

The JVM has the restriction that a class can extend only one class, but arbitrarily many interfaces. How would that work if abstract classes and traits are the same?

Well, Scala does not have the restriction, you can write class Foo extends Bar with Baz and both can be traits. Since Scala 3 has trait parameters, there is no reason to have an abstract class other than for Java-bridging, and in that case it would be probably sufficient to have an annotation if you ever needed to extend a Scala class from Java (which I have never seen in practice).

2 Likes

So you’re not removing abstract classes, just replacing a keyword with annotation.

Abstract classes have significant advantage over traits - inheritance from an abstract class is a lot cheaper than mixing in a trait. Scala’s collection hierarchy has many auxiliary abstract classes for improving compilation speed and reduction of bytecode size. ScalaTest recommends few base abstract classes instead of mixing traits in every test suite.

BTW:
(not a serious question) Why do we have objects? object SomeName extends <something> can be replaced with lazy val SomeName = new <something>. One keyword less :] And less confusion also - the object notation suggests that a value extends a type which is not true.

Scala does have the restriction: you can only mix in traits, not classes. Are there any plans to lift that restriction? The problem is the restriction is not only in Java source, but also in Byte Code, which we want to compile to. How would you get around that, compile everything into interfaces?

I don’t understand the question / problem. I already barely use abstract classes, and scalac byte code perfectly handles the case where there are only traits involved. For the Scala programmer it’s just very confusing deciding between trait and abstract class when, after traits also have parameters now, they are essentially the same, except for the very rare case where you want to ensure that you can extend a particular trait/class from Java.

2 Likes

You get around it by abolishing abstract class, and then it’s done.

This has been discussed before. A conclusion was that traits still differed from abstract classes in that traits don’t fix linearization, whereas abstract classes do. I don’t remember if there also was a conclusion that was an important argument for anything.

Regardless, it is a separate topic (that maybe needs to be split off? Mods?)

4 Likes