Kotlin's default visibility should be internal

@loganj, thanks a lot for your detailed and careful motivation. I totally see your points, but this has been a long debate since M13, and I can’t say I have too much to add to what has been said previously.

We believe that the benefits of having public by default outweigh its drawbacks, including those that you have pointed out.

I admit that using the simple measurements that I quoted in that blog post was a bit of a sloppy move, at least because quantitative studies of software are an extremely complicated business, which I had no real intention of entering. But what’s important here is that such numbers are just a quantitative justification (admittedly, rather superficial) to the experience and understanding that we have developed over time using Kotlin and Java. And I trust the latter a lot more than the former.

As an appendix: much of the reasoning presented above is more true for libraries (true libraries that have their own lifecycle with no coordination with the lifecycles of the using applications) than for application code. Unfortunately, we have one language for these two rather different use cases. Sometimes restrictions relevant for library authors may be hindering for application authors (I mentioned DSLs and primary constructors in the blog post). In such cases our general approach is that adding such restrictions (e.g. requiring explicit “public”) is better done opt-in in the tooling (like lint, inspections, etc) than in the language where it affects both camps.

5 Likes

@abreslav, welcome back, and thank you for reading and taking the time to respond!

I’m very much interested in hearing more about this comment, particularly with respect to Kotlin. I think I’ve expressed the lessons of my experience with Java pretty thoroughly, and Jesse has done a great job of filling in the gaps.

You referred to DSLs and primary constructors as examples where public-by-default is virtuous.

I understand the argument for primary constructors, but hope that there’s another solution available. Would it be unreasonable for primary constructors to inherit the class’s visibility by default? It would certainly feel natural, I think, given the declarations are adjacent. (It may be obvious here that I’m not a language designer, just an optimist.)

I admit that I don’t understand the issue with DSLs. When defining a public DSL, is it really so onerous to explicitly apply the public modifier? Are public DSLs really so common, especially relative to garden-variety classes and functions, that they should sway the default visibility?

Unfortunately, as I mentioned and Jesse expounded on, public-by-accident has much more potential to create problems than internal-by-accident. We actually will need such a lint rule and will be forced to explicitly apply modifiers to everything, public and internal alike, as a result. In effect, we won’t have a default at all!

On the other hand, if internal were the default it would be perfectly safe to forgo that lint rule and allow use of the default, and most (or at least much) of our code would need no explicit modifier.

1 Like

I think there’s two different perspectives here, trying to answer different questions:

  1. Whether or not libraries should be fully open for modification.
  2. Whether or not that should happen accidentally.

Regardless of what’s decided here, developers can still choose whether their libraries are open or closed (by specifying visibility manually). Thus, to me, the answer to the first part isn’t relevant here.

The more important question comes down to what should happen accidentally. I personally prefer “accidentally closed” to “accidentally open”, for the many reasons stated above.

5 Likes

I was thinking about this problem (public vs. internal, final vs. non-final) a lot and I believe that Mike really has some good points. The model in my mind always evolved around “how can I prevent library users from accessing internal stuff through visibility”. But actually, the only problem here is the second part (“through visibility”). It is like that because it really is about guiding users not parenting them.

Focussing on that, there could be solutions that are different from visibility. I very much like the idea of annotations for this purpose. Annotating a class with @implementationDetail (bad, fictional syntax) with some IDE magic would probably help a great deal. If those classes, functions, methods were treated in a special way, that would already help a lot:

Those symbols could have a special color treatment in code completion to show that they shouldn’t normally be used. They could be ordered last (or even appear only on a second code complete). Maybe there could even be something like a tooltip window that showed which API should be used instead (derived from a parameter of the annotation)

The Project view/structure could show them in a special way to mark them as implementation detail. To make it even clearer, you could have a compiler warning (which you probably would want to be suppressable)

On the other hand, you could still encapsulate things (like internal state that really needs to accessed through a proper access method, e.g. because of synchronization) as private. If used properly, those who would really need to use such a symbol still can (given sufficient visiblity/openness).

Python has lived a very long life with just a convention and without formal visibility modifiers. So this really is possible. I think the challenge should be to offer a better and more convenient user experience in Kotlin than in other languages.

It’s a challenge because Java code (and all other JVM languages) honor visibility. That said, stability annotations are orthogonal to visibility and should be discussed elsewhere, as I said before. There’s no reason a protected method couldn’t be marked as stable, beta, or experimental, just as a public method would.

Visibility is a Kotlin concept as it is a Java one. This discussion focuses around what the default should be to provide the most utility and have the least amount of surprise. For discussion on stability annotations and proposing them as a replacement for visibility should be scoped elsewhere.

2 Likes

They aren’t completely orthogonal because people tend to use visibility as a proxy for stability, at least in libraries. Obviously it’s impossible for Kotlin to not have visibility in it as it’s necessary for Java compatibility, and besides, the Kotlin philosophy is not to be too radical or do too much research.

I’d say that internal by default, though there are some good arguments for it, would break the principle of least surprise if only due to the name mangling that’s done for it. People would wonder why their stack traces are so messed up.

Name mangling and any other aspects of the implementation of internal visibility are entirely orthogonal to the choice of default/accidental visibility. If people are going to be surprised by internal name mangling, they’re going to be surprised whether it’s the default or not.

1 Like

Dan and Jake have perfectly summarized the core assumptions of my post.

Arguing about whether restricted visibility is inherently good or bad is entirely out of scope here, and it’s regrettable that this thread has been derailed by that topic. If you have an axe to grind against internal visibility, please start a new thread.

1 Like

With such differing opinions I almost wonder if the default visibility should be a compilation option so people can choose which default they want. The downside you would not know for certain how a piece of source code will be interpreted without knowing the compile options.

Another way to go is eliminate a default at all and always require an explicit visibility, but that I know would be a huge pain in the butt.

I think making this a compilation option would be the worst of all worlds. Now each snippet you read would need to be prefixed with “Compile with…”.

As a general rule, compiler flags are a bad place to alter the interpretation of source code.

1 Like

Some detailed story can be found in this post

3 Likes

I know that and wasn’t seriously proposing it, but it just seemed to me that the only way to satisfy such diametrically opposite positions is to either let the user choose the default or don’t default at all. Perhaps a compromise is a file annotation that lets you decide on a file by file basis the defaults for visibility and closed vs. open.

By the way we’re considering “no default at all” option that can be enabled for library developers, see this proposal: KEEP-45.

I would love to see a compiler plugin switch that would allow to make internal by default. similar to what we have with the Spring compiler plugin.
I am doing a large multi-module experiment project and found that most of the time I am having to type internal because I don’t want the module code to be available by upstream modules.

6 Likes

I too am very sad about the decisions made here, I have been working in the Kotlin language for over 3 years now, and the biggest problem is compilation time, plus class visibility. The fact that every developer in my company doesn’t think about internal vs public and just writes the “easiest code” compounds to this effect, and most of our implementation classes are visible to the entire application, and any small changes in these implementation classes breaks incremental compilation, also since they are importable, devs import the implementation instead of importing the interfaces, write dagger-module classes in wrong gradle-module units. It has been nightmare working these issues.

3 Likes

That sounds like the developers need a lot of discipline and rules instead of this being a language issue. What you can do is maybe enable API mode so that the developers are forced to specify a visibility always

1 Like

Well explicit API mode is only a thing because of Kotlin’s public default. You don’t need an explicit API mode for Java because the default is safe.

3 Likes

One solution (as compiler extension/plugin) that I can think of is using something like the allopen plugin, to make all classes internal by default, does that work as expected?

1 Like

Oh boy, that’ll cause literally all the bugs in the world. You could possibly make a checker that does that but it’ll be hard to use properly. Well actually maybe it can work but it’ll need to only error out on user-defined sources that can be edited so that you set the visibility as public manually. Again though explicit-API mode is the better option here

This breaks copy/paste since moving a file does not guarantee the same default is honored and may inadvertently widen the access level. Had the default been internal, this would have been an acceptable solution to opt-in to widening to public since the worst you can do in copy/paste is reduce visibility which is far less of a problem.

Explicit API mode is really the only recourse today.

4 Likes