Exporting imported names

Pull request

Table of contents

Abstract

In order to support exporting imported names, add export import library <library> and export <name reference> syntax.

Problem

As we develop libraries such as the prelude, we want the ability to indicate that an imported name should be re-exported for indirect use. At present, we can use the prototype alias to expose names on a case-by-case basis (alias Foo = Bar;), but it doesn’t work to export the same name (alias Foo = Foo; is a name conflict), and we want to be able to more broadly forward a library’s exported names.

For example:

package Foo library "internal";

// Declare C.
class C;
package Foo;

// We want the ability to expose everything imported here.
import library "internal";
import library Foo;

// Uses C by way of the default library.
var c: Foo.C;

Background

Some of the syntax options were discussed on Discord.

Carbon exports

Names declared in a Carbon file are currently exported by default. A private keyword may be used to prevent that export. Note C++ and TypeScript use private-by-default behavior, so the syntax choices that make sense elsewhere may not make as much sense for Carbon.

As described in the problem statement, alias offers incomplete re-export support. However, alias is not fully designed; it’s modeled on informal discussions.

Other languages

In C++ modules, this is export import ....

In TypeScript modules, similar syntax might look like:

import * from '<module>'
export * from '<module>'

In Python, names in a module are generally public, and imported names are accessible too. For example, given import datetime, the module makes the name datetime available to clients. There is interest in more explicit export syntax.

In other languages, such as Java, Kotlin, or Go, direct re-exports aren’t supported. Instead, the expectation seems to be that either a copy of the entity would be exported, or it should just be moved.

Proposal

Support the export keyword as a modifier to import library <library> (excluding cross-package imports). This is export import for short. For example:

export import library "lib";

Additionally, support the export keyword on individual, file-scoped or namespace-scoped entities (excluding entities in other packages, and namespaces themselves). This is export name for short. For example:

// Export an entity:
export Foo;
// Export an entity inside a namespace:
export NS.Bar;

// Invalid: exporting namespaces is disallowed.
export NS;

Although exporting cross-package names is disallowed, note that alias can be used to add a package-local name that originates from another package, which then is valid for export. For example:

import package Other;

// Invalid: cross-package exports are disallowed.
export Other.Obj;

// This introduces a package-local name. The alias is exported, and other
// libraries importing this library may export `Obj`.
alias Obj = Other.Obj;

The export keyword is only valid in files which are valid to import. It is invalid in files which cannot be imported: implementation files and Main//default.

Source file introduction

In the source file introduction, export import directives are intermingled with other import directives. export name directives are normal code and cannot be intermingled with any import directives, including export import directives.

This allows:

import library "foo";
export import library "wiz";
import library "bar";

export FooType;

class C { ... };

export BarType;

This disallows:

import library "foo";
// Invalid: All `import` directives must come before other code, including
// `export name`.
export FooType;

import library "bar";

class C { ... };

// Invalid: `export import` must be grouped with `import` directives.
export import library "wiz";

Future work

Namespaces

Namespaces are not valid arguments to export; entities in namespaces must be individually exported.

This keeps open a future design option of having export on a namespace export all imported names inside the namespace, such as export NS;. This could also be achieved with * syntax, such as export NS.*;. There hasn’t been discussion of this option, and this proposal takes no stance on the option.

Rationale

  • Software and language evolution
    • export <name> allows moving entities between libraries without needing to make modifications to clients, enabling more incremental refactorings.
  • Code that is easy to read, understand, and write
    • Export logic in general is intended to support factoring large or complex APIs into multiple, smaller files. For example, with the prelude, we’ll have many types, interfaces, and implementations: concise re-exporting logic will make it easy to provide a singular prelude.carbon that exports all related functionality.

Alternatives considered

Other export syntax structures

We discussed several different syntax choices.

A couple placement alternatives discussed were:

  • Put export before library. For example, import export library "lib".
    • An advantage of this is that if we support cross-package re-exports, import Foo export library "lib" could make it clearer the library is being re-exported, rather than the package.
    • A disadvantage is that we would probably not put other keywords between the package and library.
  • Put export as a suffix. For example, import library "lib" export.
    • An advantage of this is that it makes import statements line up better when some may not have the export modifier.

The current design uses export as a prefix. This is for consistency with how we put other modifier keywords, such as private or extern, prior to the introducer keyword.

A couple keyword alternatives discussed (alongside placement options) were:

  • reexport
    • An advantage noted is that it may read more intuitively for some developers.
    • This proposal suggests export because it mirrors import, and it’s consistent with multiple other languages. It’s also shorter, and Carbon often chooses keywords for shortness.
  • exported and reexported
    • These didn’t seem to read as clearly as export or reexport.

Other export name placements

We see several options for export name placement. This compares them, focusing on advantages and disadvantages for each option.

  1. export name with imports

    export name can (only) appear in the preamble, with the imports, and cannot appear with the other declarations in the library. Note this option could either have export name refer to earlier imports (creating an ordering consistency issue), or expend additional effort in order to track whether a name was already imported at the site of the export.

    Advantages:

    • No need to teach developers they cannot (don’t need to) export locally introduced names.

    Disadvantages:

    • Although the restricted placement might imply placement is tied to specific libraries, that’s not the case. This could mislead developers.
      • In theory, we could enforce this, but then we could end up breaking code if the path a name is imported through changes.
  2. export name with other declarations

    export name can only appear after imports. This means that all names valid for export will already be made available.

    Advantages:

    • import remains very special.
    • Makes it unambiguous that names valid for export are already imported.

    Disadvantages:

    • Prevents placing export name next to the import that is expected to add the name.
    • Means export import and export name will be in different sections: no single place to look for re-exports.
  3. No ordering for export name

    Let developers choose what the prefer.

    Advantages:

    • Maximum flexibility, HOA rule.

    Disadvantages:

    • Most inconsistent with the desire to treat import as special.

We’re choosing option (2). The name lookup issues avoided by requiring export be below import directives seem worthwhile.

A possible option to (2) might be to create an additional section dedicated to export name below the import section. This proposal suggests avoiding that in order to avoid increasing the amount of enforced ordering in Carbon files.

Re-exporting cross-package

As proposed, re-exporting names from other packages will not be supported. This is done to continue maintaining package boundaries, and so that names aren’t unexpectedly introduced. For example:

package Foo;

class C;
package Bar;

export import Foo;
package Wiz;

import Bar;

In the last package Wiz, it might be confusing if the name Foo.C should be introduced: typically importing Bar would put everything under the Bar namespace.

We may choose to re-examine this choice, but this proposal does not include support.