api file default-public
Table of contents
Problem
Question for leads #665: private vs public syntax strategy, as well as other visibility tools like external/api/etc. decided that methods on classes should default to public. Should api echo the similar strategy?
Background
- In C++,
structmembers default public, whileclassmembers defaultpublic. - In proposal #107: Code and name organization, an
apikeyword was used to indicate public APIs within anapifile. - In #665, it was decided that Carbon class members should default
public.- This issue was reopened to discuss alternatives in this proposal.
Proposal
APIs in the api file should default public, without need for an additional api keyword. private may be specified to designate APIs that are internal to the library, and only visible to impl files.
Nothing is necessary within impl files, and APIs there will be private unless forward declared in the api file.
Rationale based on Carbon’s goals
- Code that is easy to read, understand, and write: It will be easier for developers to understand code if APIs have semantically similar behavior when comparing the visibility of class methods to other code, and the library to other packages.
Alternatives considered
Default api to private
Default private is what was implied by api, and was the previous state.
Advantages:
- Decreases the likelihood that developers will accidentally expose APIs, because it’s an explicit choice.
- Can move functions between
apiandimplwithout visibility changing.
Disadvantages:
- The
apifile’s primary purpose is to expose APIs, and so it may be more natural for developers to assume things there are public. - Inconsistent with “default public” behavior on classes.
We are switching to default public in api files for consistency with class behaviors.
Default impl to public
Noting that we default api to public, we could similarly default impl to public.
Advantages:
- Can move functions between
apiandimplwithout visibility changing.
Disadvantages:
- Everything in an
implfile must be private unless it’s a separate definition of anapideclaration. As a consequence, everything declared in theimplfile would need to be explicitlyprivate.
In order to avoid the toil of explicitly declaring everything in the impl as private, impl will be private by default. As a consequence of being the default behavior, no private should be specified, just as public is not allowed in api files.
Note the visibility behavior can be described as making declarations the most visible possible for its context, which in api files is public, and in impl is private.
Make keywords either optional or required in separate definitions
When a prior declaration exists, keywords are disallowed in separate definitions. We could instead allow keywords, making them either optional or required. This would allow developers to determine visibility when reading a definition.
The downside of this is that optional keywords can be confusing. For example:
-
apifile:class Foo { private fn Bar(); private fn Wiz(); }; -
implfile:fn Foo.Bar() { ...impl... } private fn Foo.Wiz() { ...impl... } fn Baz() { ...impl... }
In an “optional” setup, the above is valid code. However, the lack of a private keyword on Foo.Bar may lead a developer to conclude that it’s public without checking the api file (particularly because Foo.Wiz is explicitly private), when it’s actually private. This is an accident that could also occur on refactoring; for example, removing the keyword on the impl version of Foo.Wiz would be valid but does not make it public.
A response may be to make keywords required to match, so that reading the impl file would have a compiled guarantee of correctness, avoiding confusion. However, consider a similar example:
-
apifile:class Foo { fn Bar(); private fn Wiz(); }; -
implfile:fn Foo.Bar() { ...impl... } private fn Foo.Wiz() { ...impl... } fn Baz() { ...impl... }
In this example, Foo.Bar is public, and that may lead developers to conclude that Baz is public. This could be corrected by requiring private on Baz, but we are hesitant to do that per Default impl to public.
There is still some risk of confusion if the forward declaration and separate definition are both in the api file. For example:
private fn PrintLeaves(Node node);
fn PrintNode(Node node) {
Print(node.value);
PrintLeaves(node);
);
fn PrintLeaves(Node node) {
for (Node leaf : node.leaves) {
PrintNode(leaf);
}
}
In this, a reader may read the PrintLeaves definition and incorrectly conclude that it is implicitly public because (a) it has no keywords and (b) it is in the api file. This will be addressed as part of Open question: Calling functions defined later in the same file #472.
Overall, the decision to disallow keywords on separate definitions means that impl files shouldn’t have any visibility keywords at the file scope (they will on classes), which is considered a writability improvement while keeping the api as the single source of truth for public entities, addressing readability.