blob: 83800f3accaa1cb69f9b1b093a38c1bb2df0bf04 [file] [log] [blame] [view]
# Proposal: Alias declarations for Go
Authors: Robert Griesemer & Rob Pike.
Last updated: July 18, 2016
Discussion at https://golang.org/issue/16339.
## Abstract
We propose to add alias declarations to the Go language. An alias declaration
introduces an alternative name for an object (type, function, etc.) declared
elsewhere. Alias declarations simplify splitting up packages because clients
can be updated incrementally, which is crucial for large-scale refactoring.
They also facilitate multi-package "components" where a top-level package
is used to provide a component's public API with aliases referring to the
componenent's internal packages. Alias declarations are
important for the Go implementation of the "import public" feature of Google
protocol buffers. They also provide a more fine-grained and explicit
alternative to "dot-imports".
## 1. Motivation
Suppose we have a library package L and a client package C that depends on L.
During refactoring of code, some functionality of L is moved into a new
package L1, which in turn may require updates to C. If there are multiple
clients C1, C2, ..., many of these clients may need to be updated
simultaneously for the system to build. Failing to do so will lead to build
breakages in a continuous build environment.
This is a real issue in large-scale systems such as we find at Google because
the number of dependencies can go into the hundreds if not thousands. Client
packages may be under control of different teams and evolve at different
speeds. Updating a large number of client packages simultaneously may be close
to impossible. This is an effective barrier to system evolution and maintenance.
If client packages can be updated incrementally, one package (or a small batch
of packages) at a time, the problem is avoided. For instance, after moving
functionality from L into L1, if it is possible for clients to continue to
refer to L in order to get the features in L1, clients don’t need to be
updated at once.
Go packages export constants, types (incl. associated methods), variables, and
functions. If a constant X is moved from a package L to L1, L may trivially
depend on L1 and re-export X with the same value as in L1.
```
package L
import "L1"
const X = L1.X // X is effectively an alias for L1.X
```
Client packages may use L1.X or continue to refer to L.X and still build
without issues. A similar work-around exists for functions: Package L may
provide wrapper functions that simply invoke the corresponding functions
in L1. Alternatively, L may define variables of function type which are
initialized to the functions which moved from L to L1:
```
package L
import "L1"
var F = L1.F // F is a function variable referring to L1.F
func G(args…) Result { return L1.G(args…) }
```
It gets more complicated for variables: An incremental approach still exists
but it requires multiple steps. Let’s assume we want to move a variable V
from L to L1. In a first step, we declare a pointer variable Vptr in L1
pointing to L.V:
```
package L1
import "L"
var Vptr = &L.V
```
Now we can incrementally update clients referring to L.V such that they use
(\*L1.Vptr) instead. This will give them full access to the same variable.
Once all references to L.V have been changed, L.V can move to L1; this step
doesn’t require any changes to clients of L1 (though it may require additional
internal changes in L and L1):
```
package L1
import "L"
var Vptr = &V
var V T = ...
```
Finally, clients may be incrementally updated again to use L1.V directly after
which we can get rid of Vptr.
There is no work-around for types, nor is possible to define a named type T in
L1 and re-export it in L and have L.T mean the exact same type as L1.T.
Discussion: The multi-step approach to factor out exported variables requires
careful planning. For instance, if we want to move both a function F and a
variable V from L to L1, we cannot do so at the same time: The forwarder F
left in L requires L to import L1, and the pointer variable Vptr introduced
in L1 requires L1 to import L. The consequence would be a forbidden import
cycle. Furthermore, if a moved function F requires access to a yet unmoved V,
it would also cause a cyclic import. Thus, variables will have to be moved
first in such a scenario, requiring multiple steps to enable incremental
client updates, followed by another round of incremental updates to move
everything else.
## 2. Alias declarations
To address these issues with a single, unified mechanism, we propose a new
form of declaration in Go, called an alias declaration. As the name suggests,
an alias declaration introduces an alternative name for a given object that
has been declared elsewhere, in a different package.
An alias declaration in package L makes it possible to move the original
declaration of an object X (a constant, type, variable, or function) from
package L to L1, while continuing to define and export the name X in L.
Both L.X and L1.X denote the exact same object (L1.X).
Note that the two predeclared types byte and rune are aliases for the
predeclared types uint8 and int32. Alias declarations will enable users
to define their own aliases, similar to byte and rune.
## 3. Notation
The existing declaration syntax for constants effectively permits
constant aliases:
```
const C = L1.C // C is effectively an alias for L1.C
```
Ideally we would like to extend this syntax to other declarations
and give it alias semantics:
```
type T = L1.T // T is an alias for L1.T
func F = L1.F // F is an alias for L1.F
```
Unfortunately, this notation breaks down for variables, because it already
has a given (and different) meaning in variable declarations:
```
var V = L1.V // V is initialized to L1.V
```
Instead of "=" we propose the new alias operator "=>" to solve the
syntactic issue:
```
const C => L1.C // for regularity only, same effect as const C = L1.C
type T => L1.T // T is an alias for type L1.T
var V => L1.V // V is an alias for variable L1.V
func F => L1.F // F is an alias for function L1.F
```
With that, a general alias specification is of the form:
```
AliasSpec = identifier "=>" PackageName "." identifier .
```
Per the discussion at https://golang.org/issue/16339, and based on feedback
from adonovan@golang, to avoid abuse, alias declarations may refer to imported
and package-qualified objects only (no aliases to local objects or
"dot-imports").
Furthermore, they are only permitted at the top (package) level,
not inside a function.
These restriction do not hamper the utility of aliases for the intended
use cases. Both restrictions can be trivially lifted later if so desired;
we start with them out of an abundance of caution.
An alias declaration may refer to another alias.
The LHS identifier (C, T, V, and F in the examples above) in an alias
declaration is called the _alias name_ (or _alias_ for short). For each alias
name there is an _original name_ (or _original_ for short), which is the
non-alias name declared for a given object (e.g., L1.T in the example above).
Some more examples:
```
import "oldp"
var v => oldp.V // local alias, not exported
// alias declarations may be grouped
type (
T1 => oldp.T1 // original for T1 is oldp.T1
T2 => oldp.T2 // original for T2 is oldp.T2
T3 [8]byte // regular declaration may be grouped with aliases
)
var V2 T2 // same effect as: var V2 oldp.T2
func myF => oldp.F // local alias, not exported
func G => oldp.G
type T => oldp.MuchTooLongATypeName
func f() {
x := T{} // same effect as: x := oldp.MuchTooLongATypeName{}
...
}
```
The respective syntactic changes in the language spec are small and
concentrated. Each declaration specification (ConstSpec, TypeSpec, etc.)
gets a new alternative which is an alias specification (AliasSpec).
Grouping is possible as before, except for functions (as before).
See Appendix A1 for details.
The short variable declaration form (using ":=") cannot be used to
declare an alias.
Discussion: Introducing a new operator ("=>") has the advantage of not
needing to introduce a new keyword (such as "alias"), which we can't really
do without violating the Go 1 promise (though r@golang and rsc@golang observe
that it would be possible to recognize "alias" as a keyword at the package-
level only, when in const/type/var/func position, and as an identifier
otherwise, and probably not break existing code).
The token sequence "=" ">" (or "==" ">") is not a valid sequence in a Go
program since ">" is a binary operator that must be surrounded by operands,
and the left operand cannot end in "=" or "==". Thus, it is safe to introduce
"=>" as a new token sequence without invalidating existing programs.
As proposed, an alias declaration must specify what kind of object the alias
refers to (const, type, var, or func). We believe this is an advantage:
It makes it clear to a user what the alias denotes (as with existing
declarations). It also makes it possible to report an error at the location
of the alias declaration if the aliased object changes (e.g., from being a
constant to a variable) rather than only at where the alias is used.
On the other hand, mdempsky@golang points out that using a keyword would
permit making changes in a package L1, say change a function F into a type F,
and not require a respective update of any alias declarations referring to
L1.F, which in turn might simplify refactoring. Specifically, one could
generalize import declarations so that they can be used to import and rename
specific objects. For instance:
```
import Printf = fmt.Printf
```
or
```
import Printf fmt.Printf
```
One might even permit the form
```
import context.Context
```
as a shorthand for
```
import Context context.Context
```
analogously to the renaming feature available to imports already. One of the
issues to consider here is that imported packages end up in the file scope and
are only visible in one file. Furthermore, currently they cannot be
re-exported. It is crucial for aliases to be re-exportable. Thus alias imports
would need to end up in package scope. (It would be odd if they ended up in
file scope: the same alias may have to be imported in multiple files of the
same package, possibly with different names.)
The choice of token ("=>") is somewhat arbitrary, but both "A => B" and
"A -> B" conjure up the image of a reference or forwarding from A to B.
The token "->" is also used in Unix directory listings for symbolic links,
where the lhs is another name (an alias) for the file mentioned on the RHS.
dneil@golang and r@golang observe that if "->" is written "in reverse" by
mistake, a declaration "var X -> p.X" meant to be an alias declaration is
close to a regular variable declaration "var X <-p.X" (with a missing "=");
though it wouldn’t compile.
Many people expressed a preference for "=>" over "->" on the tracking issue.
The argument is that "->" is more easily confused with a channel operation.
A few people would like to use "@" (as in _@lias_). For now we proceed with
"=>" - the token is trivially changed down the road if there is strong general
sentiment or a convincing argument for any other notation.
## 3. Semantics and rules
An alias declaration declares an alternative name, the alias, for a constant,
type, variable, or function, referred to by the RHS of the alias declaration.
The RHS must be a package-qualified identifier; it may itself be an alias, or
it may be the original name for the aliased object.
Alias cycles are impossible by construction since aliases must refer to fully
package-qualified (imported) objects and package import cycles are not
permitted.
An alias denotes the aliased object, and the effect of using an alias is
indistinguishable from the effect of using the original; the only visible
difference is the name.
An alias declaration may only appear at the top- (package-) level where it
is valid to have a keyword-based constant, type, variable, or function
declaration. Alias declarations may be grouped.
The same scope and export rules (capitalization for export) apply as for all
other identifiers at the top-level.
The scope of an alias identifier at the top-level is the package block
(as is the case for an identifier denoting a constant, type, variable,
or function).
An alias declaration may refer to unsafe.Pointer, but not to any of the unsafe
functions.
A package is considered "used" if any imported object of a package is used.
Consequently, declaring an alias referring to an object of an package marks
the package as used.
Discussion: The original proposal permitted aliases to any (even local)
objects and also to predeclared types in the Universe scope. Furthermore,
it permitted alias declarations inside functions. See the tracking issue
and earlier versions of this document for a more detailed discussion.
## 4. Impact on other libraries and tools
Alias declarations are a source-level and compile-time feature, with no
observable impact at run time. Thus, libraries and tools operating at the
source level or involved in type checking and compilation are expected to
need adjustments.
reflect package
The reflect package permits access to values and their types at run-time.
There’s no mechanism to make a new reflect.Value from a type name, only from
a reflect.Type. The predeclared aliases byte and rune are mapped to uint8 and
int32 already, and we would expect the same to be true for general aliases.
For instance:
```
fmt.Printf("%T", rune(0))
```
prints the original type name int32, not rune. Thus, we expect no API or
semantic changes to package reflect.
go/\* std lib packages
The packages under the go/\* std library tree which deal with source code will
need to be adjusted. Specifically, the packages go/token, go/scanner, go/ast,
go/parser, go/doc, and go/printer will need the necessary API extensions and
changes to cope with the new syntax. These changes should be straightforward.
Package go/types will need to understand how to type-check alias declarations.
It may also require an extension to its API (to be explored).
We don’t expect any changes to the go/build package.
go doc
The go doc implementation will need to be adjusted: It relies on package go/doc
which now exposes alias declarations. Thus, godoc needs to have a meaningful
way to show those as well. This may be a simple extension of the existing
machinery to include alias declarations.
Other tools operating on source code
A variety of other tools operate or inspect source code such as go vet,
go lint, goimport, and others. What adjustments need to be made needs to be
decided on a case-by-case basis.
## 5. Implementation
There are many open questions that need to be answered by an implementation.
To mention a few of them:
Are aliases represented somehow as “first-class” citizens in a compiler and
go/types, or are they immediately “resolved” internally to the original names?
For go/types specifically, adonovan@golang points out that a first-class
representation may have an impact on the go/types API and potentially affect
many tools. For instance, type switches assuming only the kinds of objects now
in existence in go/types would need to be extended to handle aliases, should
they show up in the public API. The go/types’ Info.Uses map, which currently
maps identifiers to objects, will require especial attention: Should it record
the alias to object references, or only the original names?
At first glance, since an alias is simply another name for an object, it would
seem that an implementation should resolve them immediately, making aliases
virtually invisible to the API (we may keep track of them internally only for
better error messages). On the other hand, they need to be exported and might
need to show up in go/types’ Info.Uses map (or some additional variant thereof)
so that tools such as guru have access to the alias names.
To be prototyped.
## 6. Other use cases
Alias declarations facilitate the construction of larger-scale libraries or
"components". For organizational and size reasons it often makes sense to split
up a large library into several sub-packages. The exported API of a sub-package
is driven by internal requirements of the component and may be only remotely
related to its public API. Alias declarations make it possible to "pull out"
the relevant declarations from the various sub-packages and collect them in
a single top-level package that represents the component's API.
The other packages can be organized in an "internal" sub-directory,
which makes them virtually inaccessible through the `go build` command (they
cannot be imported).
TODO(gri): Expand on use of alias declarations for protocol buffer's
"import public" feature.
TODO(gri): Expand on use of alias declarations instead of "dot-imports".
# Appendix
## A1. Syntax changes
The syntax changes necessary to accommodate alias declarations are limited
and concentrated. There is a new declaration specification called AliasSpec:
**AliasSpec = identifier "=>" PackageName "." identifier .**
An AliasSpec binds an identifier, the alias name, to the object (constant,
type, variable, or function) the alias refers to. The object must be specified
via a (possibly qualified) identifier. The aliased object must be a constant,
type, variable, or function, depending on whether the AliasSpec is within a
constant, type, variable, of function declaration.
Alias specifications may be used with any of the existing constant, type,
variable, or function declarations. The respective syntax productions are
extended as follows, with the extensions marked in bold:
ConstDecl = "const" ( ConstSpec | "(" { ConstSpec ";" } ")" ) .
ConstSpec = IdentifierList [ [ Type ] "=" ExprList ] **| AliasSpec** .
TypeDecl = "type" ( TypeSpec | "(" { TypeSpec ";" } ")" ) .
TypeSpec = identifier Type **| AliasSpec** .
VarDecl = "var" ( VarSpec | "(" { VarSpec ";" } ")" ) .
VarSpec = IdentList ( Type [ "=" ExprList ] | "=" ExprList ) **| AliasSpec** .
FuncDecl = "func" FunctionName ( Function | Signature ) **| "func" AliasSpec** .
## A2. Alternatives to this proposal
For completeness, we mention several alternatives.
1) Do nothing (wait for Go 2). The easiest solution, but it does not address
the problem.
2) Permit alias declarations for types only, use the existing work-arounds
otherwise. This would be a “minimal” solution for the problem. It would
require the use of work-arounds for all other objects (constants, variables,
and functions). Except for variables, those work-arounds would not be too
onerous. Finally, this would not require the introduction of a new operator
since "=" could be used.
3) Permit re-export of imports, or generalize imports. One might come up with
a notation to re-export all objects of an imported package wholesale,
accessible under the importing package name. Such a mechanism would address
the incremental refactoring problem and also permit the easy construction of
some sort of “super-package” (or component), the API of which would be the sum
of all the re-exported package APIs. This would be an “all-or-nothing” approach
that would not permit control over which objects are re-exported or under what
name. Alternatively, a generalized import scheme (discussed earlier in this
document) may provide a more fine-grained solution.