api file default-public

Pull request

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++, struct members default public, while class members default public.
  • In proposal #107: Code and name organization, an api keyword was used to indicate public APIs within an api file.
  • 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 api and impl without visibility changing.

Disadvantages:

  • The api file’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 api and impl without visibility changing.

Disadvantages:

  • Everything in an impl file must be private unless it’s a separate definition of an api declaration. As a consequence, everything declared in the impl file would need to be explicitly private.

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:

  • api file:

    class Foo {
      private fn Bar();
      private fn Wiz();
    };
    
  • impl file:

    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:

  • api file:

    class Foo {
      fn Bar();
      private fn Wiz();
    };
    
  • impl file:

    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.