Generic details 10: interface-implemented requirements (#1088)

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 <richard@metafoo.co.uk>
This commit is contained in:
josh11b
2022-05-05 15:30:40 -07:00
committed by GitHub
co-authored by Richard Smith
parent ccd5545ebf
commit 125224ab08
2 changed files with 406 additions and 6 deletions
+241 -6
View File
@@ -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)
@@ -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
@@ -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.
@@ -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
@@ -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)
+165
View File
@@ -0,0 +1,165 @@
# Generic details 10: interface-implemented requirements
<!--
Part of the Carbon Language project, under the Apache License v2.0 with LLVM
Exceptions. See /LICENSE for license information.
SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
-->
[Pull request](https://github.com/carbon-language/carbon-lang/pull/1088)
<!-- toc -->
## Table of contents
- [Problem](#problem)
- [Background](#background)
- [Proposal](#proposal)
- [Rationale based on Carbon's goals](#rationale-based-on-carbons-goals)
- [Alternatives considered](#alternatives-considered)
- [Less strict about requirements with `where` clauses](#less-strict-about-requirements-with-where-clauses)
- [Don't require `observe...is` declarations](#dont-require-observeis-declarations)
<!-- tocstop -->
## Problem
This proposal is to add the capability for an interface to require other types
to be implemented, not just the `Self` type. The interface-implemented
requirement feature also has some concerns:
- If the interface requirement has a `where` clause, there are
[concerns](#less-strict-about-requirements-with-where-clauses) about being
able to locally check whether impls satisfy that requirement.
- A function trying to make use of the fact that a type implements an
interface due to an interface requirement, or a blanket impl, may require
the compiler perform a search that we don't know will be bounded.
## Background
The first version of interface-implemented requirements for interfaces was
introduced in proposal
[#553: Generics details part 1](https://github.com/carbon-language/carbon-lang/pull/553).
## Proposal
This proposal adds two sections to the
[generics details design document](/docs/design/generics/details.md):
- [Interface requiring other interfaces revisited](/docs/design/generics/details.md#interface-requiring-other-interfaces-revisited)
- [Observing a type implements an interface](/docs/design/generics/details.md#observing-a-type-implements-an-interface)
## Rationale based on Carbon's goals
This proposal advances these goals of Carbon:
- [Language tools and ecosystem](/docs/project/goals.md#language-tools-and-ecosystem):
The motivation for this expressive power of interface requirements comes
from discussions about how to achieve symmetric behavior with interfaces
like `CommonTypeWith` from
[proposal #911: Conditional expressions](https://github.com/carbon-language/carbon-lang/pull/911).
- [Fast and scalable development](/docs/project/goals.md#fast-and-scalable-development):
The requirement that the source provide the proof of any facts that would
require a recursive search using `observe` declarations means that the
expense of that search is avoided except in the case where there is a
compiler error. If the search is successful, the results of the search can
be copied into the source, and afterward the search need not be repeated.
## Alternatives considered
### Less strict about requirements with `where` clauses
We could allow
[requirements with `where` constraints](/docs/design/generics/details.md#requirements-with-where-constraints)
to be satisfied by implementations that could be specialized, as long as the
constraints were still satisfied. Unfortunately, this is not a condition that
can be checked locally. Continuing the example from that section, consider four
packages
- A package defining the two interfaces
```
package Interfaces api;
interface A(T:! Type) {
let Result:! Type;
}
interface B(T:! Type) {
impl as A(T) where .Result == i32;
}
```
- A package defining a type that is used as a parameter to interfaces `A` and
`B` in blanket impls:
```
package Param api;
import Interfaces;
class P {}
external impl [T:! Type] T as Interfaces.A(P) where .Result = i32 { }
// Question:Is this blanket impl of `Interfaces.A(P)` sufficient
// to allow us to make this blanket impl of `Interfaces.B(P)`?
external impl [T:! Type] T as Interfaces.B(P) { }
```
- A package defining a type that implements the interface `A` with a wildcard
impl:
```
package Class api;
import Interfaces;
class C {}
external impl [T:! Type] C as Interfaces.A(T) where .Result = bool { }
```
- And a package that tries to use the above packages together:
```
package Main;
import Interfaces;
import Param;
import Class;
fn F[V:! Interfaces.B(Param.P)](x: V);
fn Run() {
var c: Class.C = {};
// Does Class.C implement Interfaces.B(Param.P)?
F(c);
}
```
Package `Param` has an implementation of `Interfaces.B(Param.P)` for any `T`,
which should include `T == Class.C`. The requirement in `Interfaces.B` in this
case is that `T == Class.C` must implement `Interfaces.A(Param.P)`, which it
does, and `Class.C.(Interfaces.A(Param.P).Result)` must be `i32`. This would
hold using the blanket implementation defined in `Param`, but the wildcard impl
defined in package `Class` has higher priority and sets the associated type
`.Result` to `bool` instead.
The conclusion is that this problem would only be detected during
monomorphization, and could cause independent libraries to be incompatible with
each other even when they work separately. These were significant enough
downsides that we wanted to see if we could live with the restrictions that
allowed local checking first. We don't know if developers will want to declare
their parameterized implementations `final` in this situation anyway, even with
[the limitations on `final`](/docs/design/generics/details.md#libraries-that-can-contain-final-impls).
This problem was discussed in
[the #generics channel on Discord](https://discord.com/channels/655572317891461132/941071822756143115/941089885475962940).
### Don't require `observe...is` declarations
We could require the Carbon compiler to do a search to discover all interfaces
that are transitively implied from knowing that a type implements a set of
interfaces. However, we don't have a good way of bounding the depth of that
search.
In fact, this search combined with conditional conformance makes the question
"is this interface implemented for this type" undecidable
[in Rust](https://sdleffler.github.io/RustTypeSystemTuringComplete/). Note: it
is possible that
[the acyclic rule](/docs/design/generics/details.md#acyclic-rule) would avoid
this problem in Carbon for blanket impls, but it doesn't apply to interface
requirements.
This problem was observed in
[a discussion in #typesystem on Discord](https://discord.com/channels/655572317891461132/708431657849585705/938167784565792848).