class and interface syntaxextends instead of extendrequire another interface without writing Self implswhere clauses after requireimpl as for interface requirementsadapter or adaptor instead of adaptexternal consistently instead of extendmix keyword for mixinsUpdate syntax of class and interface definitions to be more consistent.
Constructs that add names to the class or interface from another definition are
always prefixed by the extend keyword.
Implements the decisions in:
Classes and adapters, prior to this proposal, use impl to say that an
interface is
implemented internally,
which means that the names that are members of the interface are included as
names of the class. The keyword external is added to indicate the names should
not be included. Interfaces and named constraints, in contrast, use impl to
mean another interface is required, but its names are not included. Instead, to
include the names, the extends keyword used instead of impl.
| Include names: | Yes | No |
|---|---|---|
class, adapter |
impl |
external impl |
interface, constraint |
extends |
impl |
In the time since this syntax has been introduced, we have found external in
particular easy to accidentally omit.
In addition to resolving this inconsistency, it would be an advantage if readers of a class could quickly scan the definition to identify other places to look for members that contribute to the class' API.
These proposals that defined the syntax for these entities that are being modified:
impl
declarations may appear outside of a class definition, and external impl
declarations may appear inside.No proposal so far has defined how forward declarations work for classes. The
rule used for forward interface, constraint, and impl declarations is that
the declaration part of the definition is everything up to the opening { of
the definition body. See
the forward declaration section of the Generics details design doc
added in proposal
#1084: Generics details 9: forward declarations.
This proposal incorporates the decisions made in these question-for-leads issues:
Some of thinking around the resolution of #995 was documented in issue #2293: reconsider syntax for internal / external implementation of interfaces, which was closed as a duplicate of #995.
In addition to modifying syntax from previous proposals, #995 also gives a syntax for using a mixin in a class. Mixins are described as a use case in #561: Basic classes: use cases, struct literals, struct types, and future work, but have not been added in any proposal. Question-for-leads issue #1000: Mixins: base classes or data members?, does state that a class will treat a mixin syntactically like a data member instead of a base class.
Any declaration that adds the names from another entity shall start with the
(new) extend keyword. This includes:
Inheritance: A class now indicates that it inherits from a base class using an
extend base:base-class;
declaration inside the class definition. The extend keyword indicates that
the API of the base class is included.
Adapters: Adapter types are now declared as a class, with an
[
extend]adaptadapted-class;
declaration inside the definition, as an alternative to a base class
declaration. The optional extend keyword controls whether the API of the
adapted class is included.
Implementations: Internal implementations are marked with the extend
keyword on the declaration inside the class. Only the declaration inside the
class, which is required for internal implementations, uses the extend
keyword. External implementations are not marked.
[
extend]impl...
Interfaces: The extends declaration in an interface definition is
replaced by an extend declaration, with no change except removing the s
from the end of the keyword. Other interface requirements are now written
using a require declaration, with a constraint that matches a where
clause. This means
impl asrequired-interface;
will now be written as
require Self implsrequired-interface;
and
impltype-expressionasrequired-interface;
will now be written as
requiretype-expressionimplsrequired-interface;
For now, only the impls forms of where clauses are permitted after
require.
In summary:
| Before | After |
|---|---|
class D extends B { ... } |
class D { extend base: B; ... } |
external impl C as Sub; |
impl C as Sub; |
class C { impl as Sortable; } |
class C { extend impl as Sortable; } |
adapter A for C { ... } |
class A { adapt C; ... } |
adapter A extends C { ... } |
class A { extend adapt C; ... } |
interface I { impl as J; } |
interface I { require Self impls J; } |
interface I { impl T as J; } |
interface I { require T impls J; } |
interface I { extends J; } |
interface I { extend J; } |
None of adapter, extends, external will continue to be keywords. To match
these changes, "internal implementations" will now be referred to as "extended
implementations," and we will no longer use "external" to refer to
implementations.
In addition, we drop the syntax for conditionally implemented internal interfaces. Instead, an external interface implementation can be combined with aliases to the members of the interface.
What was previously written:
base class B;
class D extends B;
class D extends B {
...
}
is now written:
base class B;
class D;
class D {
extend base: B;
...
}
An extend base class declaration may appear in the body of a class definition, and has this form:
extendbase:type-expression;
The extend base: B; declaration must appear before any other data member
declaration, including any mixin declaration, once those
are added. This reflects both the importance of the information, and the fact
that the base subobject appears first in the memory layout of objects.
Note that base is already a keyword, for example used in base class
declarations. The colon in base: B is to indicate that base acts like a data
member for
purposes of initialization.
This makes the part of a class definition that is used in forward declarations
be exactly the part before the curly braces ({...}). Before this proposal,
class forward declarations would exclude the extends clause from the the first
line of the class definition. This change makes classes consistent with other
entities that may be forward declared.
What was previously written:
interface Sortable;
interface Add;
interface Sub;
class C;
// Forward declaration says whether external.
impl C as Sortable;
external impl C as Add;
class C {
// Internal impl contributes to the API.
impl as Sortable;
// External impl of an operator.
external impl as Add;
}
// External impl of an operator.
external impl C as Sub;
// Definition of `impl` declared earlier.
impl C as Sortable { ... }
external impl C as Add { ... }
is now written:
interface Sortable;
interface Add;
interface Sub;
class C;
// Forward declaration same whether extended or not.
impl C as Sortable;
impl C as Add;
class C {
// Extended impl contributes to the API.
extend impl as Sortable;
// (Non-extended) Impl of an operator.
impl as Add;
}
// (Non-extended) Impl of an operator.
impl C as Sub;
// Definition of `impl` declared earlier.
impl C as Sortable { ... }
impl C as Add { ... }
Whether an interface is extended or not is now only reflected in its declaration inside the class body, not in any declaration or definition outside.
An impl declaration, with this proposal, must have one of these two forms:
Without an extend keyword prefix, used for non-extended impl
declarations and for all impl declarations outside of a class body:
impl[forall[deduced-parameters]] [type-expression]asfacet-type-expression (;|{impl-body})
The type-expression is required outside of a class body, otherwise it
defaults to Self.
With an extend keyword prefix, to indicate this implementation is
extended, only in a class body:
extendimplasfacet-type-expression (;|{impl-body})
Note that this form does not allow either a forall clause nor a
type-expression before the as keyword. This reflects the restriction
that
wildcard impl declarations must never be extended (formerly: "always be external"),
and that this proposal removes support for
extended (formerly "internal") conditional implementation.
We remove direct support for conditionally implemented extended (formerly "internal") interfaces, called conditional conformance. We can work around this restriction by using an non-extended interface implementation and aliases to the members of the interface.
What was previously written:
interface Printable {
fn Print[self: Self]();
}
class Vector(T:! type) {
// ...
impl forall [U:! Printable] Vector(U) as Printable {
fn Print[self: Self]();
}
}
is now written:
interface Printable {
fn Print[self: Self]();
}
class Vector(T:! type) {
// ...
alias Print = Printable.Print;
}
impl forall [U:! Printable] Vector(U) as Printable {
fn Print[self: Self]();
}
The way this works is that Vector.Print is equivalent to
Vector.(Printable.Print), which may or may not be defined. The name
Vector.Print can no longer be conditional, and the meaning of that name is
fixed. However, the implementation of Printable for Vector(T) may not exist
for some types T.
What was previously written:
class C;
// Forward declarations of adapters.
adapter A for C;
adapter E extends C;
// Definitions of an adapter.
adapter A for C {
...
}
// Definition of an extending adapter.
adapter E extends C {
...
}
is now written:
class C;
// Forward declarations of adapters.
class A;
class E;
// Definitions of an adapter.
class A {
adapt C;
...
}
// Definition of an extending adapter.
class E {
extend adapt C;
...
}
Note:
adapt still must not contain anything that was previously
forbidden for adapters: no fields, no base class, no virtual methods, no
implementations of virtual methods, and so on.adapt declaration must appear before
mixin declarations, if any.The syntax for an adapt declaration inside a class body is:
[
extend]adapttype-expression;
What was previously written:
interface A { let T:! Type; }
interface B { let U:! Type; }
interface C(V:! Type) { }
interface I {
// `A`'s interface is incorporated into `I`:
extends A where .T = i32;
// No impact on `I`s interface, but an
// implementation must exist:
impl as B where .U = i32;
// Implementation must exist on another type:
impl i32 as C(Self);
}
is now written:
interface A { let T:! Type; }
interface B { let U:! Type; }
interface C(V:! Type) { }
interface I {
// `A`'s interface is incorporated into `I`:
extend A where .T = i32;
// No impact on `I`s interface, but an
// implementation must exist:
require Self impls B where .U = i32;
// Implementation must exist on another type:
require i32 impls C(Self);
}
Notes:
where clauses or require
declarations.Syntax for a require declaration in an interface or named constraint:
requiretype-expressionimplsfacet-type-expression;
As
with impl...as declarations before,
a require declaration must use Self, either to the left or right of
impls. Note that require only supports this subset of where clause
expressions. Adding other kinds of constraints is future work.
Syntax for an extend declaration in an interface or named constraint:
extendfacet-type-expression;
Mixins have not been defined in a proposal so far. However, part of the process
of resolving
issue #995 was
deciding on a syntax for including a mixin in a class. This was done in order to
make sure that class declarations that included names from another entity were
treated consistently, for example always starting with the extend keyword.
// Mixin declarations and definitions are
// outside the scope of this proposal.
mixin M1;
mixin M2;
class C {
// Mixing in mixin M1
extend m: M1;
// Mixing in mixin M2. This member is not named.
// Initialized using `M2.MakeDefault()`.
extend _: M2 = M2.MakeDefault();
// Alternative to the above `M2` that uses a
// private name instead of no name:
extend private m2: M2;
}
The declaration that a class uses a mixin is called a "mix" declaration. The syntax of a mix declaration is:
extendprivate|protected:mixin-expression [=initializer-expression];
The id part of the mix declaration defines the name assigned to that mixin
subobject. This name is may be used to access members of the mixin and to
initialize the mixin in a constructor for the class. The optional private or
protected access specifier controls the access to this name.
With this proposal, base class declarations appear in the body of the class
definition, like data members, so decision of whether mixins are more like base
classes or data members of issue
#1000: Mixins: base classes or data members?
is less significant. Like base classes, the mix declaration syntax begins with
extend. Like data members, a class may have multiple mix declarations and they
may be intermixed with field declarations. The layout of the memory of an object
reflects the order of the declarations in the class body, defining the order of
the mixin and field subobjects.
The main reason for the new syntax is consistency and simplification:
extend keyword is the consistent way to mark what other
entities are consulted during name lookup.class declaration is simplified by moving more into the definition
body.These consistency and simplification improvements help:
extends instead of extendKeyword extend was chosen over extends to parallel impl, a declaration,
instead of impls, a binary predicate, decided in
issue #2495 and
accepted in
proposal #2483.
We chose to use require instead of requires and adapt instead of adapts
for the same consistency.
require another interface without writing Self implsWe
considered
allowing interface I { require J; } as a short-hand for
interface I { require Self impls J; }. This is something we would consider
adding in the future based on experience with the current approach, but for now
we wanted to maintain consistency with the constraint syntax of where clauses.
This decision and rationale was described in this comment in #995.
where clauses after requireThe decision on
#995, see
1
and
2,
called for the same constraint syntax after require as we are using after
where. This proposal only allows impls clauses after require, since
there were concerns about the other kinds of clauses:
Self. or .? See
this comment thread on #2760.= instead of ==), would be
allowed in a require declaration, and if they were, whether they would be
changed into equality constraints (as if they were declared using ==). See
this comment on #2760.For now, we only needed to replace the existing uses of impl as constraints,
which had none of these concerns. We did not want to block this proposal, so we
made sure the require clauses were consistent with that subset of where
clauses.
impl as for interface requirementsThere were a few reasons motivating the change to use the new require
declarations in interfaces and named constraints, instead of using impl as to
match how a type could satisfy that requirement. These mostly came down to some
observed breaks in the parallel structure between the requirement in interfaces
and the satisfaction of that requirement in types.
I extends or just requires another interface J is
independent of whether a type implementing I extends or just implements
J.R requires interface I, the implementation of R for a type won't
have the implementation of I as a nested sub-block.impl as in interfaces is different from in classes with
respect to rewrites of associated types, motivating a change to make those
look more different.Furthermore, we had a desire to be able to express the full range of constraints
in where clauses in named constraints, and we wanted the transformation from a
where clause to a named constraint to be straightforward. We also wanted the
syntax for constraints to be the same between interfaces and named constraints,
at least for all constraints that were allowed in both.
This was discussed in #generics-and-templates on 2023-01-30 and in issue #995.
adapter or adaptor instead of adaptWe didn't want to allow both adapter and adaptor, since that adds complexity
for readers and tooling, but neither seemed clearly dominant enough in usage to
pick one over the other. By moving the declaration into the body of the class
definition, we were able to switch from the noun form to the verb form of
adapt, which doesn't have an alternate form in common usage. See
#1159: adaptor versus adapter may be harder to spell than we'd like
for the discussion.
We also considered using adapts in a class declaration, as in
class PlayableSong adapts Song { ... }, see
this comment in #1159.
This would have also worked, but was not consistent with our resolution of
#995: Generics external impl versus extends.
In the
open discussion on 2023-02-27,
we discussed some alternatives to extend adapt adapted-class ;:
Adding and to make it read more like fluent English:
extend and adaptadapted-class;
However, this felt arbitrary and not compositional.
Make the extending and adapting be separate declarations:
extendadapted-class;
adaptadapted-class;
This felt too repetitive.
Make the only way to make an extending adapter be trying to inherit from a final base class
extend base:adapted-class;
However, this meant using the same syntax for two different things that could only be distinguished by looking at the declaration of the adapted class, which could be far away. It also would have meant making an extending adapter of a non-final class much more cumbersome.
Ultimately, we decided that extend adapt would be the most compositional way
of combining extend and adapt, so that is what users would expect. Reading
like natural English was not considered essential.
The fact that there were some different rules for external implementations was
brought up in
this post in #2770.
However,
this reply
pointed out those rules could be clearly stated in terms of where you are
allowed to write extend. That same post made the convincing argument that to
get the maximum benefit of the decision on
#995, we should
treat extend and impl as separate orthogonal concepts as much as possible.
We considered two different ways of more directly supporting conditionally extending a class by an interface:
Both of these approaches would have required some support for expressing this in the new syntax. None of the syntactic approaches we considered were found to be satisfactory. By making name lookup never conditional, it made it much easier to have a consistent marker for declarations that extended name lookup.
Ultimately, the alternative of not having a dedicated syntax to support this case seemed the simplest in the short term, given the workaround of conditional external (non-extending) implementation paired with aliases that provide the name lookup, unconditionally. We can always add dedicated syntax later, given sufficient motivating information.
The options were considered in #2580: How should Carbon handle conditionally implemented internal interfaces.
external consistently instead of extendWe considered making "extending name lookup" the default and overriding that
default with the external keyword. The argument for this option rested on it
being more convenient to express conditional internal implementation, and wasn't
seen as attractive once that feature was removed. In the discussion, we
generally preferred marking additional places to consult in name lookup since
that was something we expected readers of the code to want to specifically look
for.
This option was proposed as the third option in the original #995 question, and was considered in this comment.
mix keyword for mixinsWe considered a variety of different syntax options for using a mixin for a type
in
this comment on #995.
Many of them used the mix keyword, but we ultimately decided that mix was
redundant with saying extend, which we wanted to be included with all
constructs extending the type's API by another entity.
There were three different aspects of mixins that we considered giving control over access:
This was particularly relevant when we were considering separate declarations for declaring the mixin member and the API extension (1, 2).
This approach had the problem that the common case for mixins was extending the API, which would not have been the default. The only examples we had where the mixer class would not want to include the names exported by the mixin were cases where the mixin had nothing to export. This led to the position that the mixin would control which of its member would be included into the mixer class' API -- that is the mixin would "inject" members rather than "export" them and leave it up to the mixer class to import them.
We had concerns that there might be name conflicts, but we thought those might be handled by some other mechanism. This is being considered in question-for-leads issue #2745: Name conflicts beyond inheritance.
We wanted mixin member names to behave consistently like other class member
names, and so default to public but can have a private modifier to make
private, following
#665: private vs public syntax strategy, as well as other visibility tools like external/api/etc..
We decided to put the private keyword between the extend keyword and the
member name for two reasons:
extend in a class, andprivate access control only applies to the
member name, not what the extend controls.We did not see a use case for controlling the ability to cast between the mixin and the mixer class types separately from being able to access the name the mixin member of the mixin class. This was consistent with our desire to limit declarations to a single access control specifier per declaration, see this update in #995.
By moving the base class into the class body, we accomplished three things:
extend.This was decided in this comment on #995.
This left open the question of what keyword introducer to use in base class
declarations, since using base would cause an ambiguity with declaring a
member class that could be extended, as considered in
this comment.
Ultimately we avoided this problem by requiring base class declarations to
always begin with extend, not support any form of private or protected
inheritance (see
this comment),
and not support any combination of an extend declaration with a member class
declaration.