Visibilities (post 1.0 stuff)


#1

Visibility in the JVM world has become a total mess over the years. Lately I've been pondering what sort of changes I'd like to see in Kotlin post 1.0 and how they could be achieved in a backwards compatible way. Visibility is one of the areas I've been thinking about.

In the JVM world we have, at minimum, the following types of visibility in wide usage:

  • public
  • private
  • protected
  • package-private
  • private[this] from the Scala world
  • internal from the Kotlin world (though is it actually used much?)
  • public @VisibleForTesting,  i.e. "public but you're not meant to use this, it's only for unit testing purposes"
  • public but in a package that is understood to be for internal use only, e.g. sun.misc.*
  • public @Deprecated, i.e. "public but you're not meant to use this anymore because it was replaced with something better, or found to be dangerous"
  • public @Deprecated(errorLevel=HIDDEN) in Kotlin, which means "was once public, but is now public at the binary level but not at the source level"
  • public @Deprecated(errorLevel=ERROR) in Kotlin, which means "was once public, but is now public at the binary level and at the source level but using it yields a custom error message"
  • Post-Jigsaw, public will fragment further into:
    • public but not exported from the module, i.e. internal
    • public and exported from the module

This is a zoo!

I suggest that one of the reasons dynamic languages still appeal to people, despite their obvious downsides, is that dynamic languages typically ignore visibility and avoid all this mental overhead.

Even apparently simple visibilities like private are more complex than they look: is something private to keep it out of javadocs and autocomplete lists, in which case overriding the protection would perhaps be bad form but nothing more? Or is it private for security purposes, like to avoid it being exposed by a serialisation or other reflection mapping framework, or to sandboxed code?

It’d be great if Kotlin could tackle this disaster-zone of complexity at some point post 1.0.  My suggestion for how to do this is as follows.

We have the following visibilities only:

  • public, the default, which means visible to everything and exported by the platform's module system, if there is one. In the case of Jigsaw, this means that the Kotlin compiler would generate a module-info.class file that by default exports every package.
  • internal, which becomes deprecated when applied to anything other than the package keyword. A mismatch in the internal-ness of package keywords is a compiler error. An internal package is the same as a regular package, but is not exported via the platform's module system and types within it are visible only within that code module. It is valid, for example, to do dead code elimination inside internal packages if there are no uses of the defined code. Lack of the internal package keyword anywhere means existing code functions as normal.
  • protected and private are the same as today.
  • unsupported supercedes the notion of deprecation. Deprecation implies old code that should no longer be used. A type or method marked with the unsupported visibility modifier, in contrast, may be marked so from the first version. It is useful for APIs that are deprecated, experimental, or rely on internal implementation details that the library developer does not wish to commit to but which might be useful anyway. It compiles down to a regular public method with the java @Deprecated annotation, for backwards compatibility with Java consumers, and a @kotlin.Unsupported annotation that is understood by the IDE. Unsupported method calls aren't rendered with a strikethrough like today, but may be rendered with a different foreground or background colour to reflect the fact that whilst not recommended, they are not necessarily going to go away in future either. By default they don't generate compile warnings individually but the use of one may result in a simple "You are using unsupported APIs from package X" type message. They appear in auto-completion and API docs. A new @Explanation annotation can be used to provide a one-liner that explains why it's unsupported in tooltips and auto-complete widgets.
  • hidden supercedes @Deprecated(level=HIDDEN). Hidden types or methods are public at the binary level but should not be visible at the source level at all, and attempting to use them should result in a "not found" error. They do not appear in API docs or autocompletion lists. The primary purpose of hidden is for methods that exist to support inline functions. These methods are not intended to be used directly, but must be present at runtime so the inlined code can call into them. If a hidden method shadows a non-hidden method then the non-hidden method is also considered to be hidden, that is, a hidden method in scope can prevent the use of a method with the same prototype from an outer scope. The purpose of this feature is to allow type-safe builder DSLs to prevent illegal nesting: the object can have hidden extension functions for builders that aren't applicable at that point but which would be in scope, thus taking them out of scope and preventing the DSL user from making a mistake that'd otherwise be caught at runtime.

The IDE and compiler would gain a new feature: when code is compiled for unit tests, all visiblities become public. This eliminates the need for "public @VisibleForTesting", overly-aggressive mocking and other ugly warts that unit testing sometimes introduces when blackbox just isn't good enough. Test code would then be untroubled by visibilities.

The goal of the above proposal is to meet all the use cases for different flavours of visibility observed in the wild and be compatible with Jigsaw, whilst being simple and small. The deprecation of the internal modifier anywhere other than package level may be controversial. Currently internal is implemented using name mangling and IMO isn’t really ideal. It also isn’t compatible with Jigsaw which can only (at least for now) expose entire packages, not individual types or methods within the package. Having two different incompatible notions of a module would just get really confusing for people, I think. Better for Kotlin to lose its not really emphasised custom idea of a module and wait for Jigsaw. But the internal modifier could still work and have the same meaning as today for backwards compatibility, it does not need to be removed.

I believe that this proposal would be backwards compatible and not involve removing anything.

What do you all think?


#2

While I see many of the points you are making, the reality of the JVM run-time and our type-system imposes some difficulties here. Some comments:

  • package-private can’t be eliminated, because Java has it: we couldn’t find a way around it.
  • private[this] is present in Kotlin as well as in Scala, although in an implicit form, and is, to my knowledge, inevitable, as long as we have declaration-site variance
  • by no means we want to have people confused by two misaligned notions of “module”, so we’ll do our best to align our internal with Jigsaw’s non-exported public, but I’m not sure that the mere lack of support from Jigsaw justifies removing class- and member-level internal
  • literally making everything public for tests is not so easy a task, because it affects run-time behavior: overriding works differently (and may be VERY surprising) and so on, also reflection will likely suffer. We are looking into ways of achieving this with more sophisticated techniques
  • I agree that we somewhat abuse the word “deprecated”, but it would take us some time to thoroughly think the issue through

#3

I’m not sure why declaration site variance affects the existence of private[this] but I never thought much about the implications of this type of generics, so I’ll take your word for it :smile:

I wonder if a simpler way to get the public-for-unit-tests behaviour would be to modify the JVM itself. I realise that may be out of scope for the Kotlin team, but if the JVM linker accepted a command line flag that said “allow linkage to private members” then it’d avoid the issues with overriding and reflection.