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
.
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.
This proposal adds an "Operator overloading" section to the detailed design of generics.
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.
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.
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.
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.
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.
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.
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.