A bit about picking defaults

tl;dr We are going to keep public as the default visibility, and keep classes final by default.

Introduction. Two forum threads, one about final vs open, another about public vs internal/private by default, show that these issues are rather controversial. Some people are in favor of more restrictive defaults, others are against. It seems to be connected to the differences between libraries and applications. In any case, takeaway: there’s no silver bullet here. Fortunately, neither of the decisions seems to bring much harm on, so we, as language designers, are left to our intuitions and considerations like “which would drive fewer people away from the language?” and “is migrating everyone to the new default prohibitively difficult?”

So, the best would be a solution that doesn’t burden the application writers and doesn’t harm the library writers. I would say that library writers could probably sustain even a little more inconvenience than application writers, because they want more safety and because they are simply a minority of users (although a super-important and often more vocal minority). Being myself part of this minority, I don’t feel it’s wrong saying it aloud. We aim at minimizing the inconveniences, of course, and what we can’t avoid, can be somewhat compensated for by lint-like rules and IDE inspections.

Now, having declared our values thereof: “very convenient for applications, convenient enough for libraries”, let me explain our design choices. All dogma aside, plain use cases. (I think I was the one who started throwing dogma-based arguments into this discussion, by referring to Effective Java, so lesson learned: if something aligns with your design choice, it doesn’t mean the choice was based on it, so don’t bring it up.)

Visibility: We used to have internal by default at some point, only it wasn’t checked by the compiler, so it walked like public and talked like public. Then we tried switching the checking on, and realized how much public we needed to add to our code. Application code, not library code. It was a lot of public :slight_smile: We analyzed the cases, and it turned out that it was not due to carelessly arranged module boundaries. Modules were perfectly logical, and still there were very many classes that got really ugly compared to what they were because of all those public keywords everywhere.

Primary constructors and DSLs based on delegated properties suffered the worst: every property bore a visual burden of public repeated over and over again, and we’d put a lot of effort to make those really smooth, so it was a disappointment.

Thus, we realized that members of a class have to be as visible by default as the class itself. Note that if a class is internal, its public members are effectively internal as well. So, we had two options:

  • either the default visibility is public, or
  • classes have a different default visibility from that of their members.

In the latter case the default would change for a function depending on whether it is declared on the top level or in a class. We decided to keep it consistent, and settled for public.

For library authors, there may be an optional lint rule and an IDE inspection to make sure all public’s are explicit in the code (this is does not have to apply for members of internal classes). This arguably leaves library writers with no default visibility for their public code, but it looks like less of a problem to us than having inconsistent defaults or too much public’s in application code.

Open vs final: We started by having final by default, because the initial design of Kotlin included multiple inheritance for classes, and an open class was a lot more expensive bytecode-wise than a final class, so we wanted to minimize the number of open classes. Then, the multiple inheritance idea was discarded (but we kept inheritance by delegation, which many Kotlin users find handy), and the default stayed.

We have been living with this default for quite a bit of time by now, but it looks somewhat inconsistent with the default visibility: there we choose the most permissive one, here — the most restrictive one.

So, what keeps us from making everything open by default? Well, for one thing, there is a rather significant migration cost: open occurs rarely in Kotlin, so basically all classes and their members that exist now will have to be marked final. On top of that there are other things in the language that interact here:

  • simple final val’s can be subject to smart casts, while open ones can not (they may be overridden in unknown subclasses, and not be stable any longer) — in our experience this may affect the elegance of the code quite a bit;
  • an open var can’t have a private setter;
  • subclasses of a sealed class may themselves be open or not, but the intention is most of the time that they are final, otherwise the intuitive default understanding of a sealed class as one with the known set of descendants (not only direct subclasses) may be broken too often.

These are little things, but very annoying, and they would pop up in everyday work of virtually every Kotlin developer. We think that this is too much of a sacrifice.

So, a generous gesture doesn’t work, and we are down to use cases now. There is a complaint that keeps coming back regularly: “when I want an open class, I want to open many members, and it gets all covered in open”. This is largely backed by libraries and frameworks like Mockito and Spring AOP which want to subclass something and intercept every method in it: unlike with visibility, members of an open class are not open by default, and the modifier has to be repeated for each member.

We could make a compromise and say that classes are final by default, but members of non-final classes are open. We experimented with it on our code bases, and the problems listed above are not ubiquitous, but still pop up annoyingly often: in all abstract, open and sealed classes. It would be sacrificing an occasional convenience of the majority of users (everybody uses smart casts and virtually everybody wants private setters) for occasional convenience (not everything needs to be mocked/intercepted) of a minority of users.

So, all we can do is add some modifier that says “all members of this class are open by default”, and this can be done after 1.0.

Conclusion. I would like to reiterate that these decisions are inherently debatable and likely to cause a lot of bikeshedding discussions. We are going after our intuitions here trying to optimize most common use cases and not harm other use cases much. Our time budget looking for better balance here is almost up, but what we have now looks good enough to me.

22 Likes

Thanks Andrey. This is a very well written summary and it’s interesting how some of the design decisions were tried out early on, before many of us showed up.

Also, I for one hadn’t realised open vs final actually affected the usability of the language in other ways like smart casting. That does indeed change the equation quite a bit.

3 Likes

I’m late to the party, but my two cents.

I’m in favor of final by default, but I understand the case for open by default.

For me, “fixing” libraries should be done via delegation (you’re much less likely to do harm via overriding + delegation than via overriding + inheritance).

So you couldn’t subclass final classes, except via delegation.

Like often, the problem seems to be what you expose to Java (since with this way of doing things you can’t make the classes final in Java anymore).

Thanks for considering these issues.

Regarding open vs. final, would it be possible for classes and their members to be effectively final by default within Kotlin itself without being marked final in the generated .class files? In other words, classes and their members would not really be final, but the Kotlin compiler would nonetheless prevent extension of classes and methods that are not explicitly opened. This would fix the issues with Spring and Mockito integration while providing a degree of safety regarding the three issues described in the post.

Someone could still go into Java and cause one of the problems that you’ve mentioned, but I think that risk may be worth it to allow runtime code generation without the need to manually open everything. And there is already a precedent for relaxing safety guarantees when integrating with Java code (e.g. relaxed null-checks for Java types).

1 Like

I think mocking shouldn’t be an issue here. Kotlin needs a proper mocking framework like JMockit, which can mock final methods without ceremony.

And maybe Kotlin developers should look for alternative technologies in other cases as well. Spring AOP is a poor man’s AOP which limits the possible solutions. AspectJ, for example, is able to intercept final methods. The restrictions of Mockito, Spring AOP and the like are leading to weaker code in Java, too. So, look for something better.

How exactly is this going to provide a degree of safety? Kotlin fully supports Java interop. Therefore, extending a Kotlin class with a Java class is a perfectly legal operation. Therefore, we cannot rely on the fact that the class will not have any inheritors, and cannot allow smartcasts for properties or benefit in other ways from knowing that the class is final.

1 Like