A uniform view on visibility

It seems that package private modifier is a much desired feature from the community. In this thread, I want to present a structured view on visibility in general and how package private and potential
other modifiers could fit into the picture. Via a structured and uniform approach, I want to avoid proliferation of modifiers which is looming in the background.

Abstract View

Consider a tree with root at the top and leaves at the bottom. You can think of the root as the user and the other nodes representing modules, kt files, classes, functions and properties. When finding a path from node A to node B in that tree, we say that A is visible to B. By default, any node is visible to any other node. We can freely move up and down the tree.

Now we want to restrict visibility by restricting how far we can go up the tree, starting from a certain node. How do we want to encode such information into the tree? The ad-hoc answer could be to attach that information to any node from which we want to start. E.g. at node X we attach the information that we only can go up 3 steps to node Y. However, this information is not invariant with respect to subtrees. The latter means that when looking at any subtree, the information within it is still complete and makes sense. If we cut off the branch just one step above X in the example just mentioned, the information on X is not valid anymore.

Instead, I propose to place restricting information at the barrier nodes directly. In the example above, the information should be attached to Y saying that the node with relative path A.B.X (starting from Y) cannot travel beyond Y. We could also say that Y hides A.B.X. In that sense, Y acts as a kind of firewall. When we demand that Y hides A.B, it would hide A.B.X implicitly.

For direct children, I would still allow the reversed style. So if X is a direct descendant of Y, instead of
saying that Y hides X, we still can attach that information to X directly. We say that X is private, implicitly referring to its direct parent Y.

Mapping to Kotlin

Now we want to make the step from the abstract view to the Kotlin world. We call the intermediate nodes in a tree above visibility containers (VC). Currently, the following things are visibility containers in Kotlin:

  • modules
  • kt files
  • classes

There are the obvious containment rules between these containers and these represent the edges in the tree.

Now the private in the above discussion obviously maps to the private in Kotlin. It also has the appropriate behavior. A private member in a class or in a file is only visible in that class or in that file. What is missing, however, is the possibility to say that a direct child of a module (a file), is private to that module.

The more general way to restrict visibility via the hides mechanism is absent in Kotlin. Note that internal is the reverse way (bottom-up instead of top-down) to define such an information. The same goes for potential modifiers such as “file-private”, “package-private” or “internal protected”. They all can be replaced by one concept (hide).

The protected modifier is a special case supporting derived containers and is not affected by this discussion.

Examples

Files could be declared as private via an annotation.

@file:Private
package org.example

This avoids the repitition of internal in the file. More importantly, it assures that such a file does not leak elements to the public API.

Hide something in a file which is not a top level declaration:

package org.example

import kotlin.math.min

hide MyClass.fileInternalMethod

class MyClass {
    fun fileInternalMethod() {
        // ...
    }
}

Hide something in a class which is contained in a nested class:

class MyClass {
    hide NestedClass.helper
    
    class NestedClass {
        fun helper() {
            // ...
        }
    }
}

Hide a class in a module (this assumes the existence of something like a module-info.kt in the root of the module):

// in module-info.kt
hide org.example.MyClass

Avoiding hide

It should be a rule that hide should be used judiciously, if at all. Very often, when one thinks that it is necessary, a redesign can help to avoid it. Two examples follow.

hide Domain.internalMethod

class Domain {
    fun apiMethod() {
        // ...
    }
    fun internalMethod() {
        // ...
    }
}

Instead of exposing the whole class, an interface could be introduced which only contains the non-hidden method.

interface Domain {
    companion object {
        operator fun invoke(): Domain = DomainImpl()
    }
    fun apiMethod()
}

private class DomainImpl : Domain {
    override fun apiMethod() {
        // ...
    }
    fun internalMethod() {
        // ...
    }
}

Another example:

hide PublicEnum.mapped

private enum class PrivateEnum {
    ALPHA, BETA
}
enum class PublicEnum(val mapped: PrivateEnum) {
    ONE(PrivateEnum.ALPHA), TWO(PrivateEnum.BETA)
}

Redesign:

enum class PublicEnum2 {
    ONE, TWO
}
private val PublicEnum2.mapped get() = when(this) {
    PublicEnum2.ONE -> PrivateEnum.ALPHA
    PublicEnum2.TWO -> PrivateEnum.BETA
}

Note: Since internal is the equivalent of a hide, the discussion in this section also applies to it.
I want to make the bold claim (knowing that there are always exceptions) that private on all possible levels should be enough in a well-designed code base.

Packages

In Kotlin, packages are not visibility containers, only namespace containers. But we could just declare them to be visibility containers and then follow the rules. We should also decide whether packages are considered flat (as in Java) or hierarchical, meaning that packages have other packages as children, besides kt files.

The annotation on files

@file:Private
package org.example

then would refer to the package org.example saying that the file is hidden within the package and, consequently, also hidden within the module.

Hide declarations would be placed into a package-info.kt file.

// in some package-info.kt file
hide MyClass.myMethod

Links