Skip to content

Commit

Permalink
Generic details 10: interface-implemented requirements (#1088)
Browse files Browse the repository at this point in the history
This proposal:

- Adds support for interfaces requiring other types than `Self` to implement interfaces, as in:
  ```
  interface IntLike {
    impl i32 as As(Self);
    // ...
  }
  ```
- Defines requirements on how to satisfy those requirements that have a `where` clause.
- Extends `observe` declarations to include saying a type implements an interface, so code can provide a proof instead of the compiler having to perform a recursive search.

Co-authored-by: Richard Smith <[email protected]>
  • Loading branch information
josh11b and zygoloid authored May 5, 2022
1 parent ccd5545 commit 125224a
Show file tree
Hide file tree
Showing 2 changed files with 406 additions and 6 deletions.
247 changes: 241 additions & 6 deletions docs/design/generics/details.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
- [Interface members with definitions](#interface-members-with-definitions)
- [Interface defaults](#interface-defaults)
- [`final` members](#final-members)
- [Interface requiring other interfaces revisited](#interface-requiring-other-interfaces-revisited)
- [Requirements with `where` constraints](#requirements-with-where-constraints)
- [Observing a type implements an interface](#observing-a-type-implements-an-interface)
- [Observing interface requirements](#observing-interface-requirements)
- [Observing blanket impls](#observing-blanket-impls)
- [Operator overloading](#operator-overloading)
- [Binary operators](#binary-operators)
- [`like` operator for implicit conversions](#like-operator-for-implicit-conversions)
Expand Down Expand Up @@ -1191,6 +1196,9 @@ def DoHashAndEquals[T:! Hashable](x: T) {
**Comparison with other languages:**
[This feature is called "Supertraits" in Rust](https://doc.rust-lang.org/book/ch19-03-advanced-traits.html#using-supertraits-to-require-one-traits-functionality-within-another-trait).

**Note:** The design for this feature is continued in
[a later section](#interface-requiring-other-interfaces-revisited).

### Interface extension

When implementing an interface, we should allow implementing the aliased names
Expand Down Expand Up @@ -3753,12 +3761,6 @@ types, possibly with different implementations.

In general, `X(T).F` can only mean one thing, regardless of `T`.

**Concern:** The conditional conformance feature makes the question "is this
interface implemented for this type" undecidable in general.
[This feature in Rust has been shown to allow implementing a Turing machine](https://sdleffler.github.io/RustTypeSystemTuringComplete/).
The acyclic restriction may eliminate this issue, otherwise we will likely need
some heuristic like a limit on how many steps of recursion are allowed.

**Comparison with other languages:**
[Swift supports conditional conformance](https://github.com/apple/swift-evolution/blob/master/proposals/0143-conditional-conformances.md),
but bans cases where there could be ambiguity from overlap.
Expand Down Expand Up @@ -4812,6 +4814,238 @@ There are a few reasons for this feature:

Note that this applies to associated entities, not interface parameters.

## Interface requiring other interfaces revisited

Recall that an
[interface can require another interface be implemented for the type](#interface-requiring-other-interfaces),
as in:

```
interface Iterable {
impl as Equatable;
// ...
}
```

This states that the type implementing the interface `Iterable`, which in this
context is called `Self`, must also implement the interface `Equatable`. As is
done with [conditional conformance](#conditional-conformance), we allow another
type to be specified between `impl` and `as` to say some type other than `Self`
must implement an interface. For example,

```
interface IntLike {
impl i32 as As(Self);
// ...
}
```

says that if `Self` implements `IntLike`, then `i32` must implement `As(Self)`.
Similarly,

```
interface CommonTypeWith(T:! Type) {
impl T as CommonTypeWith(Self);
// ...
}
```

says that if `Self` implements `CommonTypeWith(T)`, then `T` must implement
`CommonTypeWith(Self)`.

The previous description of `impl as` in an interface definition matches the
behavior of using a default of `Self` when the type between `impl` and `as` is
omitted. So the previous definition of `interface Iterable` is equivalent to:

```
interface Iterable {
// ...
impl Self as Equatable;
// Equivalent to: impl as Equatable;
}
```

When implementing an interface with an `impl as` requirement, that requirement
must be satisfied by an implementation in an imported library, an implementation
somewhere in the same file, or a constraint in the impl declaration.
Implementing the requiring interface is a promise that the requirement will be
implemented. This is like a
[forward declaration of an impl](#declaring-implementations) except that the
definition can be broader instead of being required to match exactly.

```
// `Iterable` requires `Equatable`, so there must be some
// impl of `Equatable` for `Vector(i32)` in this file.
external impl Vector(i32) as Iterable { ... }
fn RequiresEquatable[T:! Equatable](x: T) { ... }
fn ProcessVector(v: Vector(i32)) {
// ✅ Allowed since `Vector(i32)` is known to
// implement `Equatable`.
RequiresEquatable(v);
}
// Satisfies the requirement that `Vector(i32)` must
// implement `Equatable` since `i32` is `Equatable`.
external impl Vector(T:! Equatable) as Equatable { ... }
```

In some cases, the interface's requirement can be trivially satisfied by the
implementation itself, as in:

```
impl [T:! Type] T as CommonTypeWith(T) { ... }
```

Here is an example where the requirement of interface `Iterable` that the type
implements interface `Equatable` is satisfied by a constraint in the `impl`
declaration:

```
class Foo(T:! Type) {}
// This is allowed because we know that an `impl Foo(T) as Equatable`
// will exist for all types `T` for which this impl is used, even
// though there's neither an imported impl nor an impl in this file.
external impl Foo(T:! Type where Foo(T) is Equatable) as Iterable {}
```

This might be used to provide an implementation of `Equatable` for types that
already satisfy the requirement of implementing `Iterable`:

```
class Bar {}
external impl Foo(Bar) as Equatable {}
// Gives `Foo(Bar) is Iterable` using the blanket impl of
// `Iterable` for `Foo(T)`.
```

### Requirements with `where` constraints

An interface implementation requirement with a `where` clause is harder to
satisfy. Consider an interface `B` that has a requirement that interface `A` is
also implemented.

```
interface A(T:! Type) {
let Result:! Type;
}
interface B(T:! Type) {
impl as A(T) where .Result == i32;
}
```

An implementation of `B` for a set of types can only be valid if there is a
visible implementation of `A` with the same `T` parameter for those types with
the `.Result` associated type set to `i32`. That is
[not sufficient](/proposals/p1088.md#less-strict-about-requirements-with-where-clauses),
though, unless the implementation of `A` can't be specialized, either because it
is [marked `final`](#final-impls) or is not
[parameterized](#parameterized-impls). Implementations in other libraries can't
make `A` be implemented for fewer types, but can cause `.Result` to have a
different assignment.

## Observing a type implements an interface

An [`observe` declaration](#observe-declarations) can be used to show that two
types are equal so code can pass type checking without explicitly writing casts,
without requiring the compiler to do a unbounded search that may not terminate.
An `observe` declaration can also be used to show that a type implements an
interface, in cases where the compiler will not work this out for itself.

### Observing interface requirements

One situation where this occurs is when there is a chain of
[interfaces requiring other interfaces](#interface-requiring-other-interfaces-revisited).
During the `impl` validation done during type checking, Carbon will only
consider the interfaces that are direct requirements of the interfaces the type
is known to implement. An `observe...is` declaration can be used to add an
interface that is a direct requirement to the set of interfaces whose direct
requirements will be considered for that type. This allows a developer to
provide a proof that there is a sequence of requirements that demonstrate that a
type implements an interface, as in this example:

```
interface A { }
interface B { impl as A; }
interface C { impl as B; }
interface D { impl as C; }
fn RequiresA[T:! A](x: T);
fn RequiresC[T:! C](x: T);
fn RequiresD[T:! D](x: T) {
// ✅ Allowed: `D` directly requires `C` to be implemented.
RequiresC(x);
// ❌ Illegal: No direct connection between `D` and `A`.
// RequiresA(x);
// `T` is `D` and `D` directly requires `C` to be
// implemented.
observe T is C;
// `T` is `C` and `C` directly requires `B` to be
// implemented.
observe T is B;
// ✅ Allowed: `T` is `B` and `B` directly requires
// `A` to be implemented.
RequiresA(x);
}
```

Note that `observe` statements do not affect the selection of impls during code
generation. For coherence, the impl used for a (type, interface) pair must
always be the same, independent of context. The
[termination rule](#termination-rule) governs when compilation may fail when the
compiler can't determine the impl to select.

### Observing blanket impls

An `observe...is` declaration can also be used to observe that a type implements
an interface because there is a [blanket impl](#blanket-impls) in terms of
requirements a type is already known to satisfy. Without an `observe`
declaration, Carbon will only use blanket impls that are directly satisfied.

```
interface A { }
interface B { }
interface C { }
interface D { }
impl [T:! A] T as B { }
impl [T:! B] T as C { }
impl [T:! C] T as D { }
fn RequiresD(T:! D)(x: T);
fn RequiresB(T:! B)(x: T);
fn RequiresA(T:! A)(x: T) {
// ✅ Allowed: There is a blanket implementation
// of `B` for types implementing `A`.
RequiresB(x);
// ❌ Illegal: No implementation of `D` for type
// `T` implementing `A`
// RequiresD(x);
// There is a blanket implementation of `B` for
// types implementing `A`.
observe T is B;
// There is a blanket implementation of `C` for
// types implementing `B`.
observe T is C;
// ✅ Allowed: There is a blanket implementation
// of `D` for types implementing `C`.
RequiresD(x);
}
```

In the case of an error, a quality Carbon implementation will do a deeper search
for chains of requirements and blanket impls and suggest `observe` declarations
that would make the code compile if any solution is found.

## Operator overloading

Operations are overloaded for a type by implementing an interface specific to
Expand Down Expand Up @@ -5431,5 +5665,6 @@ parameter, as opposed to an associated type, as in `N:! u32 where ___ >= 2`.
- [#990: Generics details 8: interface default and final members](https://github.com/carbon-language/carbon-lang/pull/990)
- [#1013: Generics: Set associated constants using `where` constraints](https://github.com/carbon-language/carbon-lang/pull/1013)
- [#1084: Generics details 9: forward declarations](https://github.com/carbon-language/carbon-lang/pull/1084)
- [#1088: Generic details 10: interface-implemented requirements](https://github.com/carbon-language/carbon-lang/pull/1088)
- [#1144: Generic details 11: operator overloading](https://github.com/carbon-language/carbon-lang/pull/1144)
- [#1146: Generic details 12: parameterized types](https://github.com/carbon-language/carbon-lang/pull/1146)
Loading

0 comments on commit 125224a

Please sign in to comment.