tl;dr We are going to keep
public as the default visibility, and keep classes
final by default.
Introduction. Two forum threads, one about
open, another about
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 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
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.