carbon-lang icon indicating copy to clipboard operation
carbon-lang copied to clipboard

`impl` declarations in a generic class context

Open josh11b opened this issue 7 months ago • 7 comments
trafficstars

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 impl is being extended. In this case the type to the left of the as must be omitted and always is Self.
  • For ergonomic reasons, such as convenient/shorter access to things declared in that scope, or wanting to acll attention to this impl for readers of the class.
  • For declaration order reasons, it can be helpful to have the flexibility to place impl declarations 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

josh11b avatar Apr 04 '25 20:04 josh11b

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.

josh11b avatar Apr 04 '25 21:04 josh11b

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.

josh11b avatar Apr 04 '25 21:04 josh11b

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).

josh11b avatar Apr 04 '25 21:04 josh11b

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.

josh11b avatar Apr 04 '25 21:04 josh11b

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.

josh11b avatar Apr 04 '25 21:04 josh11b

Clarifications of the last option:

  • Every use of the Self type (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 Self type.

    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));
    

josh11b avatar Apr 06 '25 01:04 josh11b

FWIW, the resolution in this comment makes sense to me.

chandlerc avatar Apr 15 '25 04:04 chandlerc