Generics details 6: remove facets
Table of contents
Problem
There were some concerns about facet types leaking out of generic code in return types. Some initial fixes for this were done in PR #900, but there remain concerns, for example when associated types are involved.
In particular, given an interface method with return type using an associated type, as in:
interface Deref {
let Result:! Type;
fn DoDeref[me: Self]() -> Result;
}
class IntHandle {
impl Deref {
let Result:! Type = i32;
fn DoDeref[me: Self]() -> Result { ... }
}
}
Since Result
has type Type
, we had the problem that IntHandle.DoDeref
would have to return i32 as Type
, instead of the desired i32
.
We also think we can simplify the model by eliminating the facet type concept and syntax.
Proposal
This proposal removes facet types, introduces archetypes in their place, clarifies how associated types work outside of a generic function, and specifies how a generic let
statement in a function body works.
Details
The details of this proposal are in the changes to these generics design documents:
Rationale based on Carbon’s goals
This proposal adds a goal about minimizing the differences between a regular function and one with a generic parameter from the perspective of the caller. This is to support the software and language evolution goal by reducing the amount of changes needed to source code when generalizing a function.
This change is also a simplification to the programming model, removing a concept that developers have to learn and reducing the number of types a program deals with. This is in support of the code that is easy to read, understand, and write goal.
Alternatives considered
This design was the conclusion of a number of discussions and proposals:
- #typesystem discussion in Discord on Nov 2
- Nov 3, 2021 document by
zygoloid
titled Member lookup in generic and non-generic contexts - Nov 4, 2021 document by
josh11b
andmconst
titled Carbon: facets exposed from generics - Open discussion on 2021-11-08
- Open discussion 2021-11-11
The main alternatives we evaluated are summarized below.
Status quo: name lookup only in the type-of-type
The main problem with the status quo is that we would look up names in the type-of-type whether or not the type value was known. This meant that there was no way to have a type variable that didn’t alter the API of the type it held, even when the value of that variable was known at compile time. It also created problems putting a particular facet type into an associated type, as was desired when computing the CommonType
of two facets of the same type, since the type-of-type of the associated type would overwrite the facet. This last concern was raised in this #typesystem discussion in Discord on Nov 2.
Status quo: facet types
Facet types as a user-visible type expression MyType as MyConstraint
have a few downsides:
- Facet types introduce extra types for the user to concern themselves with.
- Facet types introduce extra types for instantiation purposes.
- We don’t want the possibility of facet types leaking outside of generic code.
Archetypes have replaced using facet types to type check a function with a generic parameter. Generic let
and adapter types address trying to repeatedly access methods from an interface implemented externally.
We considered making the other changes in this proposal with a different way of forming a facet type than MyType as MyConstraint
in this document. Ultimately we all agreed that we did not want to instantiate templates called with a generic type with a facet type, and the removal of facet types simplifies the design.
Name lookup in type and type-of-type
zygoloid
proposed a solution of looking up names in both the type and the type-of-type, resolving conflicts in the same way &
would between two type-of-types. This has the nice property of using the type information when it is available, but still allowing the type-of-type to affect name lookup. This made the code inside and outside a generic consistent as long as there were no name conflicts. The downside is that this still involved types potentially changing when you added a generic parameterization, even though the changes were smaller, and didn’t have the simplification advantages of removing facet types entirely.
We did like the idea from that proposal that we would perform look up in the type when it was a constant, since that addressed the main problem we were trying to address.
No generic ‘let’
Generic let
has two main use cases: manual monomorphizing a generic and making repeated calls to members of an interface implemented externally more convenient. The former may be performed by using qualified member names, and the latter by an adapter type, so this feature is not strictly needed.
However, chandlerc
believes including generic let
is more consistent, for example with the use of generic let
to declare associated types in interfaces and implementations. It make replicating the change in type behavior of calling a generic function much more straightforward, without having to introduce a function call or rewriting all of the member accesses to add qualification.
Removing generic let
is a simplification we would consider in the future.
Generic ‘let’ erases connection to original type
We considered the idea that a generic let
would fully erase type identity, on discord as “option A”. This didn’t have any clear advantages other than making a generic let
in a function body be more similar to a generic function parameter. Erasing the type identity would have made generic let
much harder to use, though, without any clear ways to get values of the new type. The option we chose is more similar to generic let
used to declare an associated type.