Consistent class and interface syntax

Pull request

Table of contents

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:

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:

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.

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 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 as required-interface ;

    will now be written as

    require Self impls required-interface ;

    and

    impl type-expression as required-interface ;

    will now be written as

    require type-expression impls required-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.

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-extended impl declarations and for all impl 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 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.

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 or require 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-expression impls facet-type-expression ;

    As with implas 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:

    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 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.
  • 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 interface J is independent of whether a type implementing I extends or just implements J.
  • If R requires interface I, the implementation of R for a type won’t have the implementation of I 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 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.

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.