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

How should Carbon handle conditionally implemented internal interfaces #2580

Closed
chandlerc opened this issue Feb 5, 2023 · 1 comment
Closed
Labels
leads question A question for the leads team

Comments

@chandlerc
Copy link
Contributor

chandlerc commented Feb 5, 2023

Summary of issue:

Carbon currently has a design for internal implementation of interfaces where the API of that interface is included into the API of the type doing the implementation. It also has the concept of conditionally implementing interfaces. When combined, these create difficult concepts to resolve in name lookup that motivate considering different approaches to the design.

Details:

Today you can write code like the following:

interface Printable {
  fn Print[self: Self]();
}

class Vector(T: type) {
  // ...

  impl forall [U:! Printable] Vector(U) as Printable {
    fn Print[self: Self]();
  }
}

The idea is that Vector(T) implements Printable only when T does (perhaps so that the generic Vector type can delegate to Ts implementation. That, in isolation, makes sense. But it also appears to conditionally include the method Vector(T).Print based on whether T implements Printable which in turn would mean that name lookup into Vector(T) would depend on which interfaces T implements, which seems very undesirable for name lookup. Generally, we would like to have a separation of concerns here to decouple the question of the names found when looking into Vector(T)'s scope, and the conditional implementation of interfaces.

I think there are three broad approaches here:

  1. We can make name lookup conditional and give up on the separation of concerns here. That would make the current design "just work" but seems like a unnecessarily complex and difficult model.
  2. We can simply disallow conditional implementation of internal interfaces. This would mean that conditional implementation can only be done externally which has none of these problems.
  3. We can fully separate these features, 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.

So far, we've been pursuing (3) with hypothetical syntax discussed in the context of #995 that looks something like:

class Vector(T:! type) {
  // ...

  // Extend the API to include that of `Printable` but without
  // providing or requiring an implementation. Using this API
  // when an implementation is not available would be an error.
  extends Printable;
}

// Conditionally implement the API for certain `T`s.
impl forall [U:! Printable] Vector(U) as Printable {
  // ...
}

However, none of the syntaxes discussed have really ended up satisfactory. The syntax extends Printable seems like too strong of a statement and implies that in fact the interface would be implemented.

This is also a generally advanced feature that we don't expect to be widely used and yet it would end up with very convenient (but unlikely to be desired) syntax in the language.

Another approach, let's call it (3b), is to use some form of aliasing facility to achieve this. We can almost do this without anything (and so in approach (2) above) today:

class Vector(T:! type) {
  // ...

  alias Print = Printable.Print;
}

// Conditionally implement the API for certain `T`s.
impl forall [U:! Printable] Vector(U) as Printable {
  // ...
}

This almost works in the explorer, but crashes: https://carbon.compiler-explorer.com/z/xh5x7nxj7

In some ways, (3b) seems like the cleanest approach from a design perspective. However, to achieve the goal of making it easy to replicate common and expected collections of APIs for a type it would need a special way to automatically alias the entire interface. If we solve that, the semantic model here seems the most clean approach proposed here. We could imagine an aliasing approach analogous to extends impl (as suggested syntactically for non-conditional cases in #995) such as: extends aliasing Printable;.

However, after the open discussion, I've thought more about this and am largely unconvinced that we need to pursue this at all. I feel like the use case we're chasing here of composing commonly associated units of API is unlikely to be widespread when conditional.

I'd like to propose we instead go with (2) and disallow internal conditional implementation. Then we can see whether in practice this is something we want, especially because we can work-around the limitation manually with aliases where desired. What do folks think about this?

@chandlerc
Copy link
Contributor Author

I'd like to propose we instead go with (2) and disallow internal conditional implementation. Then we can see whether in practice this is something we want, especially because we can work-around the limitation manually with aliases where desired. What do folks think about this?

Let's call this decided. None of the leads have expressed concern here and we've discussed several times. This decision is also very safe -- it is a conservative stance that is easily reversed if and when we get information that motivates that, and it is in fact structured to try and ensure we do get that information if it is a problem.

zygoloid added a commit that referenced this issue Jun 15, 2023
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:

-   [#995: Generics external impl versus extends](#995),
-   [#1159: adaptor versus adapter may be harder to spell than we'd like](#1159),
-   [#2580: How should Carbon handle conditionally implemented internal interfaces](#2580), and
-   [#2770: Terminology for internal and external implementations](#2770).

Co-authored-by: Richard Smith <richard@metafoo.co.uk>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
leads question A question for the leads team
Projects
None yet
Development

No branches or pull requests

1 participant