Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generic details 10: interface-implemented requirements #1088

Merged
merged 23 commits into from
May 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
zygoloid marked this conversation as resolved.
Show resolved Hide resolved

```
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