Generic details 11: operator overloading
Table of contents
Problem
C++ supports operator overloading, and we would like Carbon to as well. This proposal is about the general problem, not the specifics application to any particular operator.
This proposal does not attempt to define a mechanism by which we can ensure that a < b
has the same value as b > a
.
Background
The generics feature is the single static open extension mechanism in Carbon, and so will be what we use operator overloading. We have already started specifying the ability to extend or customize the behavior of operators by implementing interfaces, as in these proposals:
- #820: Implicit conversions
- #845: as expressions
- #911: Conditional expressions
- #1083: Arithmetic expressions
Proposal #702: Comparison operators specified using interfaces for overloading the comparison operators, but did not pin down specifically what those interfaces are.
Proposal
This proposal adds an “Operator overloading” section to the detailed design of generics.
Rationale based on Carbon’s goals
This proposal advances Carbon’s goals:
- Code that is easy to read, understand, and write, by making common constructs more concise, and allowing the syntax to more closely mirror notation used math or the application domain.
- Software and language evolution, since this allows standard types to implement operators using the same mechanisms that user types do, this allows changes between what is built-in versus provided in a library without user-visible impact.
Alternatives considered
The current proposal requires the user to define a reverse implementation, and recommends using an adapter to do that more conveniently. We also considered approaches that would provide the reverse implementation more automatically.
Weak impls instead of adapters for reverse implementations
We proposed weak impls as a way of defining blanket impls for the reverse impl that did not introduce cycles. We rejected that approach due to giving the reverse implementation the wrong priority. This meant that there were many situations where a < b
and b > a
would give different answers.
Default impls instead of adapters for reverse implementations
We then proposed default impls as a way to define reverse implementations. These were rejected because they had a lot of overlap with blanket impls, making it difficult to describe when to use one over the other, and because they introduced a lot of complexity without fully solving the priority problem. Most of the complexity was from the criteria for determining whether the default implementation would be used. As noted, the current proposal still has some priority issues, but this way the relevant impls are visible in the source which will hopefully make it clearer why it happens.
The capability provided by default impls – the ability to conveniently give implementations of other interfaces – may prove useful enough that we would reconsider this decision in the future.
Allow an impl declaration with like
to match one without
We considered allowing an impl declared with like
to match the equivalent impls without like
. The main concern was there would not be a canonical form without like
, particularly of how the newly introduced parameter would be written. We thought we might say that the like
declaration, since it omits a spelling of the parameter, is allowed to match any spelling of the parameter. However, there would still be a question of whether to use a deduced parameter, as in [T:! ImplicitAs(i64)] Vector(T)
or not as in Vector(T:! ImplicitAs(i64))
. We also considered the canonical form of Vector(_:! ImplicitAs(i64))
without naming the parameter. In the end, we decided to start with a restrictive approach with the knowledge that we could change once we gained experience.
The main use case for allowing declarations in a different form, which may motivate changes in the future, is to prioritize the different implementations generated by the like
shortcut separately in match_first
blocks.
This was discussed in open discussion on 2022-03-24.
Where are the impl definitions from like
generated?
We considered whether the additional impl definitions would be generated with the first declaration of an impl using like
or with its definition. We ultimately decided on the former approach for two reasons:
- The generated impl definitions are parameterized even if the explicit definition is not, and parameterized impl definitions may need to be in the API file to allow separate compilation.
- This will make the code doing implicit conversions visible to callers, allowing it to be inlined, matching how the caller does implicit conversions for method calls.
This was discussed in the #generics channel on Discord.
Support marking interfaces or their members as external
We discussed on 2022-03-28 the idea that operator interfaces might be marked external
. This would either mean that types would only be able to implement them using external impl
or that even if they were implemented internally, they would not add the names of the interface members to the type. Alternatively, individual members might be marked external
to indicate that their names are not added to implementing types, which might also be useful for making changes to an interface in a compatible way.
We were not sure if this feature was needed, so we left this as future work.