Replace keyword is with impls

Pull request

Table of contents

Abstract

Use the keyword impls instead of is when writing a where constraint that a type variable needs to implement an interface or named constraint.

What was previously (provisionally) written:

fn Sort[T:! Container where .ElementType is Ordered](x: T*);

will now be written:

fn Sort[T:! Container where .ElementType impls Ordered](x: T*);

Problem

The is keyword has been used as a placeholder in where constraints expressing that a type variable is required to implement an interface or named constraint. It has been an open question what word should be used in that position since its original adoption in #818. Since then, reasons to use a different word in this position have been discovered:

  • The word “is” is unspecific and could represent many different relationships.
  • This specific relationship is not symmetric.
  • We potentially want to use is as a keyword for another purpose.
  • The precedent for is came from Swift where x is T means “x has the type T”. With the changes to Carbon generic semantics, particularly #2360: Types are values of type type, that is increasingly a poor fit for what we mean by this condition.

Background

The is keyword as a constraint operator was introduced in #818: Constraints for generics (generics details 3), along with the open question about how to spell it.

The choice of is in that proposal followed is being Swift’s type check operator, where x is T is true if x has type T. Note that there are differences between the is operator in Swift and what we have used it for in Carbon. In Swift, it is used to test whether a value dynamically has a specific derived type, when you have a value of a base class type and are using inheritance. In Carbon:

  • it is used on types instead of values;
  • it is about conformance to an interface (the equivalent of Swift’s protocols), and not about inheritance; and
  • it is resolved at compile time.

Proposal

Use the keyword impls instead of is when writing a where constraint that at type variable needs to implement an interface or named constraint. The specific changes are included in the same PR as this proposal.

Rationale

This proposal is working towards Carbon’s code that is easy to read, understand, and write goal:

  • Examples read naturally using “implements” in the place of the impls keyword, commonly matching how a comment would describe the constraint.
  • More clearly communicates the relationship between the two sides and that the relationship is not symmetric.
  • If a function has a where T impls C constraint that is not satisfied for some calling type T, the fix is for the caller to add an impl T as C definition.

Alternatives considered

One concern with using impls is the potential for confusion with the plural of impl, meaning “implementations,” rather than acting as the verb “implements.” We hope to mitigate that concern by avoiding use of “impls” to mean anything other than impls in our documentation. For example, we would say “impl declarations” instead of “impls”.

Alternatives were considered in #2495: Keyword to use in place of is in whereis constraints. A number of alternatives were considered:

  • T is C
  • T isa C
  • T impls C
  • T implements C
  • T ~ C
  • T: C
  • T as C
  • T impl C
  • T impl as C
  • impl T as C

The reasons against is were outlined in the problem and rationale sections. Reasons against other alternatives:

  • isa seems (much) too rooted in inheritance.
  • implements is long but otherwise fine. This is a specific place where being verbose has worrisome negative impact.
  • ~ is already being considered for something a bit more fitting – bidirectional convertibility of our type “equality” constraints.
  • impl seems a bit clunky, and surprising to see in this position when it usually isn’t.
  • impl as seems even more clunky
  • impl T as C also seems even more clunky, and would be confusing with the rules around rewrite constraints (different here from the use of impl T as C in a type).

In general there were concerns that the alternatives were confusing and risk appearing to mean something other than what it does.

T as C

The keyword as received more consideration:

  • Already a thing, and names the facet that is required to exist.
  • Already used in a facet-like-but-not-facet context for impl T as C { ... }.
  • Short, easy to read, etc.

It had some disadvantages, though:

  • Doesn’t connect readers as directly and effectively to the need for an impl to satisfy the constraint.
  • Doesn’t read as nicely in context: T:! C where C.ElementType as AddWith(i32). This was a big consideration in deciding against using as.
  • Underlying the above disadvantage, it doesn’t fit into the model of a boolean expression that should be true. Instead, it is a cast that should be possible or meaningful, which is somewhat different from the rest of the things in a where clause.
    • However, “rewrite” constraints don’t quite fit this model either.
    • When using == constraints, they don’t actually imply any boolean expression that would return true. In fact, at least my understanding is that the T == U constraint could be written as a boolean expression but it would return false even when the constraint holds.

T: C

T: C matches how Swift and Rust write this, and is similar to the way you’d write the same constraint in an ordinary declaration, T:! I. This syntactic similarity is also a liability due to the differences in semantics when used in a where clause: the where clause doesn’t make the names from I available in T, and it doesn’t propagate where .A = B rewrites from I to T.

There’s also some concern that the use of : would make parameter lists hard to read when they contain embedded where clauses, like T:! Container where .ElementType: Printable, U:! OtherConstraint.