Consistent class
and interface
syntax
Table of contents
- Abstract
- Problem
- Background
- Proposal
- Details
- Future work: mixins
- Rationale
- Alternatives considered
- Use
extends
instead ofextend
- Allow interfaces to
require
another interface without writingSelf impls
- Allow other kinds of
where
clauses afterrequire
- Continue to use
impl as
for interface requirements - Continue to use
adapter
oradaptor
instead ofadapt
- Use some other syntax for extending adapters
- Continue to have some term for “external” or non-extended implementations
- More direct support for conditionally implementing internal interfaces
- Use
external
consistently instead ofextend
- Use
mix
keyword for mixins - Allow more control over access to mixins
- List base class in class declaration
- Use
Abstract
Update 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:
- #995: Generics external impl versus extends,
- #1159: adaptor versus adapter may be harder to spell than we’d like,
- #2580: How should Carbon handle conditionally implemented internal interfaces, and
- #2770: Terminology for internal and external implementations.
Problem
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.
Background
These proposals that defined the syntax for these entities that are being modified:
- #553: Generics details part 1 defined the syntax for classes implementing interfaces, internally or externally, and the syntax for named constraints (then “structural interfaces”) and interfaces requiring or extending other interfaces.
- #731: Generics details 2: adapters, associated types, parameterized interfaces defined the syntax for adapters and extending adapters.
- #1084: Generics details 9: forward declarations allowed forward declaration of implementations, so internal
impl
declarations may appear outside of a class definition, andexternal impl
declarations may appear inside. - #777: Inheritance defined the syntax for a class to extend a base class.
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:
- #995: Generics external impl versus extends,
- #1159: adaptor versus adapter may be harder to spell than we’d like,
- #2580: How should Carbon handle conditionally implemented internal interfaces, and
- #2770: Terminology for internal and external implementations.
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.
Proposal
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
]adapt
adapted-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 theextend
keyword. External implementations are not marked.[
extend
]impl
… -
Interfaces: The
extends
declaration in an interface definition is replaced by anextend
declaration, with no change except removing thes
from the end of the keyword. Other interface requirements are now written using arequire
declaration, with a constraint that matches awhere
clause. This meansimpl as
required-interface;
will now be written as
require Self impls
required-interface;
and
impl
type-expressionas
required-interface;
will now be written as
require
type-expressionimpls
required-interface;
For now, only the
impls
forms ofwhere
clauses are permitted afterrequire
.
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.
Details
Class inheritance
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:
extend
base
:
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 first line of the class definition. This change makes classes consistent with other entities that may be forward declared.
Class implementing an interface
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-extendedimpl
declarations and for allimpl
declarations outside of a class body:impl
[forall
[
deduced-parameters]
] [type-expression]as
facet-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:extend
impl
as
facet-type-expression (;
{
impl-body}
)Note that this form does not allow either a
forall
clause nor a type-expression before theas
keyword. This reflects the restriction that wildcardimpl
declarations must never be extended (formerly: “always be external”), and that this proposal removes support for extended (formerly “internal”) conditional implementation.
Class 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
.
Adapters
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:
- Adapters are now a special case of classes, not a distinct top-level declaration.
- Classes with
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. - The
adapt
declaration must appear before mixin declarations, if any. -
The syntax for an
adapt
declaration inside a class body is:[
extend
]adapt
type-expression;
Interfaces
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:
- The same change applies to named constraints, and is intended to be used in the future for named predicates used as template constraints.
- One syntax for constraints in either
where
clauses orrequire
declarations. - Want to open up the syntax to expressing more general constraints.
-
Syntax for a
require
declaration in an interface or named constraint:require
type-expressionimpls
facet-type-expression;
As with
impl
…as
declarations before, arequire
declaration must useSelf
, either to the left or right ofimpls
. Note thatrequire
only supports this subset ofwhere
clause expressions. Adding other kinds of constraints is future work. -
Syntax for an
extend
declaration in an interface or named constraint:extend
facet-type-expression;
Future work: mixins
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:
extend
[private
|protected
] (_
|id):
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.
Rationale
The main reason for the new syntax is consistency and simplification:
- The use of the
extend
keyword is the consistent way to mark what other entities are consulted during name lookup. - The
class
declaration is simplified by moving more into the definition body. - Making adapters a kind of class removes a kind of top-level declaration, a simplification, and matches how base classes are declared, a consistency.
- Dropping the class conditional implementation is a simplification.
These consistency and simplification improvements help:
- ease the implementation of language tools and ecosystem by reducing the size of the language, and providing regularities implementations can use to reuse code or more easily identify relevant parts of the code for queries;
- make Carbon code easy to read, understand, and write.
- teaching the language, since there is less to learn.
Alternatives considered
Use extends
instead of extend
Keyword 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.
Allow interfaces to require
another interface without writing Self impls
We 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.
Allow other kinds of where
clauses after require
The 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:
- We had questions about what syntax to use to refer to members of the interface, such as associated types, to constrain them. Must they start with
Self.
or.
? See this comment thread on #2760. - Modifying the constraints on an associated type after the declaration for that associated type was not something we clearly wanted to allow. Allowing that would mean having to read the whole interface definition body to understand a single member.
- It was unclear if rewrite constraints (using
=
instead of==
), would be allowed in arequire
declaration, and if they were, whether they would be changed into equality constraints (as if they were declared using==
). See this comment on #2760. - It would have provided more different ways of declaring equivalent interfaces. This concern was raised in 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.
Continue to use impl as
for interface requirements
There 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.
- Whether an interface
I
extends or just requires another interfaceJ
is independent of whether a type implementingI
extends or just implementsJ
. - If
R
requires interfaceI
, the implementation ofR
for a type won’t have the implementation ofI
as a nested sub-block. - With the change in #2173: Associated constant assignment versus equality, the behavior of
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.
Continue to use adapter
or adaptor
instead of adapt
We 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.
Use some other syntax for extending adapters
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 adapt
adapted-class;
However, this felt arbitrary and not compositional.
-
Make the extending and adapting be separate declarations:
extend
adapted-class;
adapt
adapted-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.
Continue to have some term for “external” or non-extended implementations
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.
More direct support for conditionally implementing internal interfaces
We considered two different ways of more directly supporting conditionally extending a class by an interface:
- Prior to this proposal, both name lookup and the implementation were conditional on the same condition. This has the disadvantage of entangling the concerns of name lookup and implementation, and was considered a complex and difficult model.
- We also considered fully separating these features, and allowing a type to separately extend its API with an interface and then only conditionally implement that interface. This would allow name lookup to succeed unconditionally, but in cases where the names were in fact not implemented it would produce an error.
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.
Use external
consistently instead of extend
We 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.
Use mix
keyword for mixins
We 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.
Allow more control over access to mixins
There were three different aspects of mixins that we considered giving control over access:
- the inclusion of names exported by the mixin into the mixer class,
- the ability to cast between the mixin and the mixer class types, and
- the ability to use the name of the mixin given by the mixer class to access members of the mixin.
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:
- to make it easier to scan for all uses of
extend
in a class, and - to make it clearer that the
private
access control only applies to the member name, not what theextend
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.
List base class in class declaration
By moving the base class into the class body, we accomplished three things:
- made forward declarations shorter,
- made it possible to inherit from a type declared inside the body of the class definition, and
- allowed greater consistency, all API extensions are now declarations in the body of the class definition starting with
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.