carbon-lang
carbon-lang copied to clipboard
`impl` declarations in a generic class context
Summary of issue:
Given an impl declaration in a generic class, or in a class in a generic context, we need a rule that says what to do with the generic parameters from the context. For example:
class Outer(X:! I) {
class Inner(Y:! J) {
impl <Some Type> as <Some Facet Type>;
fn F() -> (X, Y);
}
}
Other kinds of entities are accessed by their name, such as the function F in the example, and so there is no way to reference them without specifying the argument values to use for the generic. In this case Outer(A).Inner(B).F() returns a pair with type (A, B). An impl is found by type, though, so there needs to be some story for what to do with X and Y in the impl declaration (or definition).
Details:
There are a few reasons for an impl to be declared inside a class' scope:
- Being a member of the class gives access to the private internals of the class.
- The
implis beingextended. In this case the type to the left of theasmust be omitted and always isSelf. - For ergonomic reasons, such as convenient/shorter access to things declared in that scope, or wanting to acll attention to this
implfor readers of the class. - For declaration order reasons, it can be helpful to have the flexibility to place
impldeclarations in more places.
Any other information that you want to share?
This issue was initially brought up in #generics-and-templates in Discord on 2025-03-26 and discussed further in open discussion on 2025-03-27
One option under consideration: all of the generic parameters need to appear in a deducible position in the impl declaration.
class Outer(X:! I) {
class Inner(Y:! J) {
impl C(X) as I(Y);
}
}
This allowed a lot of flexibility for how X and Y would be determined from the query that found this impl, and a lot of flexibility for what queries could be included in a class scope. However, a lot of this flexibility didn't seem motivated: using X as something other than the argument to Outer and Y as something other than the argument to Inner seemed strange.
Another option: Disallow using any of the generic parameters except those that appear in deducible positions in the impl declaration.
class Outer(X:! I) {
class Inner(Y:! J) {
impl C(X) as I {
// ✅ Allowed but can't use `Y` here.
}
// ❌ Invalid use of `Y` in only non-deducible positions.
impl D(X) as J where .T = Y;
}
}
There wasn't any expressed support for this option, and it was expected to have significant implementation challenges.
Another option: Only allow impl declarations of the current class in its scope. The values of generic parameters are determined purely by deduction from the type to the left of the as. Within the scope of the impl declaration, those generic parameters may have more specific values than the rest of the class scope.
class Outer(X:! I) {
class Inner(Y:! J) {
// ❌ Forbidden, can only `impl Inner(`...`) as`...
impl C(X) as I;
// ✅ Allowed
impl Inner(bool) as J where .T = Y;
// `Y` is deduced to be `bool`, so this is equivalent to:
impl Inner(bool) as J where .T = bool;
// Note that `Inner(`...`)` is really `Outer(X).Inner(`...`)`, so `X` can be deduced.
impl forall [Z:! type] Inner(Vector(Z)) as I {
// ✅ Also allowed, `Y` is `Vector(Z)` in this scope
}
}
}
The main benefit of this approach is that it supports implementing interfaces for specializations of the class. It does not support deduction from the interface to the right of the as, though, which we anticipate to be useful, as in impl i32 as ImplicitAs(Self).
Another option: impl declarations in a class scope are not parameterized by the parameters of the containing generic context.
class Outer(X:! I) {
class Inner(Y:! J) {
// ❌ Forbidden, `Y` is not in scope.
impl Inner(Y) as I;
// ✅ All parameterized impls would use `forall`
impl forall [A:! I, B:! J] Outer(A).Inner(B) as I;
// ❌ Can't use `Self` here, due to it implicitly referencing `X` and `Y`.
impl Self as J where .T = Self;
// ❌ Means `extend impl` is forbidden when there are generic parameters
extend impl as K;
}
}
The limitations of this approach seem too severe. If we really wanted consistency around the syntax of parameterized impls, it would be more consistent to remove the forall (particularly since it was only added because of types used to be able to start with [, but no longer can).
It was also desirable to be consistent with the general rule that inner entities capture outer generic parameters.
The option we currently favor in discussion is to say the impl declaration must use Self in some deducible position of the impl declaration:
class Outer(X:! I) {
class Inner(Y:! J) {
// ✅ The following are all equivalent, and are considered to use
// `Self` as the type before `as` (which is a deducible position).
impl as I;
impl Self as I;
impl Inner(Y) as I;
impl package.Outer(X).Inner(Y) as I;
// This last declaration makes it clear we can deduce both `X`
// and `Y`. This shows that `extend impl as` is allowed.
// ✅ However `Self` can appear in other places:
impl Vector(Self) as I;
impl i32 as ImplicitAs(Self);
}
}
We were not worried about wanting to declare an impl of an unrelated class in the class scope.
This doesn't address the specialization use case, that is left as future work. There was some concern about cases like:
class C(T:! type) {
// ❌ Should `T` be `bool` or `u64`?
impl Vector(C(bool)) as ImplictAs(Vector(C(u64));
}
We felt like we could proceed without solving this issue for now.
Clarifications of the last option:
-
Every use of the
Selftype (potentially spelled different ways) will have the same parameterization. So:class Outer(X:! I) { class Inner(Y:! J) { impl Self as AddWidth(Self); } }would match the same queries as this declaration outside of the class:
class Outer(X:! I) { class Inner(Y:! J) { } } impl forall [X:! I, Y:! J] Outer(X).Inner(Y) as AddWidth(Outer(X).Inner(Y)); -
Other parameterizations of the same class other than the Self specific do not count as a use of
Self.class Outer(X:! I) { class Inner(Y:! J) { // ❌ Invalid, no use of `Self` impl forall [A:! I, B:! J] Outer(A).Inner(B) as I; } }However, this can be used to create independent uses of the
Selftype.class Outer(X:! I) { class Inner(Y:! J) { impl forall [A:! I, B:! J] Self as AddWith(Outer(A).Inner(B)); } }would match the same queries as this declaration outside of the class:
class Outer(X:! I) { class Inner(Y:! J) { } } impl forall [X:! I, Y:! J, A:! I, B:! J] Outer(X).Inner(Y) as AddWidth(Outer(A).Inner(B));
FWIW, the resolution in this comment makes sense to me.