Exporting imported names
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.carbonthat exports all related functionality.
- 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
Alternatives considered
Other export syntax structures
We discussed several different syntax choices.
A couple placement alternatives discussed were:
- Put
exportbeforelibrary. 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.
- An advantage of this is that if we support cross-package re-exports,
- Put
exportas a suffix. For example,import library "lib" export.- An advantage of this is that it makes
importstatements line up better when some may not have theexportmodifier.
- An advantage of this is that it makes
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
exportbecause it mirrorsimport, and it’s consistent with multiple other languages. It’s also shorter, and Carbon often chooses keywords for shortness.
exportedandreexported- These didn’t seem to read as clearly as
exportorreexport.
- These didn’t seem to read as clearly as
Other export name placements
We see several options for export name placement. This compares them, focusing on advantages and disadvantages for each option.
-
export namewithimportsexport namecan (only) appear in the preamble, with the imports, and cannot appear with the other declarations in the library. Note this option could either haveexport namerefer to earlierimports (creating an ordering consistency issue), or expend additional effort in order to track whether a name was already imported at the site of theexport.Advantages:
- No need to teach developers they cannot (don’t need to)
exportlocally 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.
- No need to teach developers they cannot (don’t need to)
-
export namewith other declarationsexport namecan only appear after imports. This means that all names valid forexportwill already be made available.Advantages:
importremains very special.- Makes it unambiguous that names valid for
exportare already imported.
Disadvantages:
- Prevents placing
export namenext to the import that is expected to add the name. - Means
export importandexport namewill be in different sections: no single place to look for re-exports.
-
No ordering for
export nameLet developers choose what the prefer.
Advantages:
- Maximum flexibility, HOA rule.
Disadvantages:
- Most inconsistent with the desire to treat
importas 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.