From 11dd187daa64b19f8b63d96dad68cf13e188d9ec Mon Sep 17 00:00:00 2001 From: Mikhail Zabaluev Date: Wed, 24 Oct 2018 05:25:08 +0300 Subject: [PATCH 01/13] Created binary_ops_specialization RFC --- text/0000-binary-ops-specialization.md | 183 +++++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 text/0000-binary-ops-specialization.md diff --git a/text/0000-binary-ops-specialization.md b/text/0000-binary-ops-specialization.md new file mode 100644 index 00000000000..88e0b1a667d --- /dev/null +++ b/text/0000-binary-ops-specialization.md @@ -0,0 +1,183 @@ +- Feature Name: binary_ops_specialization +- Start Date: 2018-10-20 +- RFC PR: +- Rust Issue: + +# Summary +[summary]: #summary + +Provide new traits for overloading binary operators, such as +`PartialEq2`, `PartialOrd2`, `Add2`, `AddAssign2`, etc., +with default blanket implementations abstracted over `Borrow` +for built-in and standard library types. This will drastically reduce +the number of explicit impl items necessary to cover compatible type pairs. +When resolving the implementation for an operator, the compiler will +consider the new traits along with the old school non-specializable traits. + +# Motivation +[motivation]: #motivation + +Operator overloading brings a lot of convenience into usage of data types. +When a Rust type is one of multiple representations of the same underlying +data type (usually indicated by implementing the same `Borrow`), it makes +sense to define binary operator trait impls that work between each pair of +these types. However, with proliferation of special-purpose representations +of widely used data types, such as byte arrays and strings, the number of +possible such pairs undergoes a combinatorial explosion. + +Specialization of blanket trait implementations could be used to deal with this +problem. These two impls of `PartialEq` could automatically enable equality +comparison for `String` on the left hand side and any type on the +right hand side that implements `Borrow`: + +```rust +impl PartialEq for String { + fn eq(&self, other: &str) -> bool { + &self[..] == other + } +} + +impl PartialEq for String +where + Rhs: ?Sized + Borrow, +{ + default fn eq(&self, other: &Rhs) -> bool { + &self[..] == other.borrow() + } +} +``` + +However, introducing default impls for already defined operator traits +is a breaking change: there are crates that don't restrict their +binary operator type pairs to ones sharing the same `Borrow` target. +One example is `bytes` defining `PartialEq` impls that allow comparing +`Bytes` and the standard string types. While such data domain crossing is +problematic for other reasons (e.g. differences in `Hash` for values that +compare as equal), the change should not break crates doing what has not been +forbidden. New operator traits with blanket default impls abstracted over +`Borrow` can provide a migration path and lay down discipline. + +# Guide-level explanation +[guide-level-explanation]: #guide-level-explanation + +## New operator traits +[new-operator-traits]: #new-operator-traits + +This proposal adds second-generation traits for all binary operators in +the standard library where the right-hand operand type is defined generically. +The traits are named `PartialEq2`, `PartialOrd2`, `Add2`, etc. and defined +with the same method signatures as their Rust 1.0 counterparts. Example +for `PartialEq2`: + +```rust +pub trait PartialEq2 { + fn eq2(&self, other: &Rhs) -> bool; + fn ne2(&self, other: &Rhs) -> bool { !self.eq(other) } +} +``` + +## Default blanket implementation rule +[default-blanket-implementation-rule]: #default-blanket-implementation-rule + +The rule for any crate defining a type that needs to work as an argument type +in binary operators is to define default impls of the new-style operator +traits described in this RFC, where this type is the `&self` or `self` +operand type, and a generic type parameter bound by `Borrow` defines the +other operand's type: + +```rust +impl PartialEq2 for String +where + Rhs: ?Sized + Borrow, +{ + default fn eq2(&self, other: &Rhs) -> bool { + &self[..] == other.borrow() + } +} +``` + +The type parameter of the `Borrow` bound is the basic data type that `Self` +can also be borrowed as (which can always be just `Self`). The role of `Borrow` +therefore extends to stratifying operand types of binary operators +available for the implementing type. +Notably, the standard library already maintains this stratification in the +provided implementations of `Borrow` and Rust 1.0 operator traits. + +## Overload resolution +[overload-resolution]: #overload-resolution + +When picking the implementation for an operator backed by the proverbial +traits `Op` and `Op2`, the compiler will consider the available trait +implementations in the following order: + +1. Fully specialized impls of `Op2`; +2. Fully specialized impls of `Op`; +3. Default impls of `Op2`; +4. Default impls of `Op`. + +## Path for future migration +[path-for-future-migration]: #path-for-future-migration + +The Rust 1.0 binary operator traits can(?) be deprecated +after the new traits are introduced. In the future backward-incompatible +Rust 2.0, the new traits will lose their `2` name suffix and replace the +old school operator traits. + +# Reference-level explanation +[reference-level-explanation]: #reference-level-explanation + +The proposed system allows legacy Rust 1.0 operator trait implementations +to coexist with the new blanket implementations in a backward-compatible way. +Specialized implementations of new style traits can be defined when practical, +within the `Borrow` bound of the default implementation. Crates can also +choose to provide default (or even fully specialized blanket) impls of +the legacy traits, but new-style impls should be preferred in new APIs. + +The interleaved, specialized-first overload resolution rule is designed to +prevent "spooky action at a distance" where e.g. adding a blanket impl of +`Add2` for type `A` defined in one crate could shadow an existing +`impl Add for A` in another crate that defines `B`. The situation where +non-generic impls of `Add` and `Add2`, defined in different crates, could +apply to the same pair of types, is impossible due to acyclicity of crate +dependencies and the orphan rule. + +# Drawbacks +[drawbacks]: #drawbacks + +The second-generation traits add complexity, especially to operator +overload resolution. It's likely that both new and old school trait impls +will have to be provided side by side, which increases the possibility of +implementation errors. This takes further the precedent set by the +[specialization RFC][rfc1210] that multiple different implementations may be +considered to fit one use. + +# Rationale and alternatives +[rationale-and-alternatives]: #rationale-and-alternatives + +The proposed [rule][default-blanket-implementation-rule] of defining +operator impls slashes the combinatorial explosion of mostly tedious +operator trait implementations seen today, leaving reasonable flexibility +in which operand type pairs are allowed (with `Borrow` as the guiding force). + +Addition of second-generation traits on top of the existing system +provides a backward-compatible migration path for the Rust 1.x timeframe. + +If opt-in feature gates were possible in the stable channel, the new default +impls could be defined for the Rust 1.0 traits and hidden behind a feature +gate. It's unclear to the author if this could work without the need for +all crates in the dependency graph to be compatible with the feature. + +# Prior art +[prior-art]: #prior-art + +Labeling second-generation APIs with suffix `2` to allow coexistence +with the legacy APIs is common. + +The circumstances that led to the design seem unique to Rust. + +# Unresolved questions +[unresolved-questions]: #unresolved-questions + +Is it feasible to implement overload resolution in the compiler as proposed? + +[rfc1210]: ./1210-impl-specialization.md From ae616eb97d653ca0ffcbd26885ab91d80ebdcde6 Mon Sep 17 00:00:00 2001 From: Mikhail Zabaluev Date: Thu, 25 Oct 2018 00:28:02 +0300 Subject: [PATCH 02/13] Expanded on the default impl rule Added more explanation, details, and motivational impetus for the default blanket implementation rule. --- text/0000-binary-ops-specialization.md | 42 ++++++++++++++++++++------ 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/text/0000-binary-ops-specialization.md b/text/0000-binary-ops-specialization.md index 88e0b1a667d..3f29ba2d3e9 100644 --- a/text/0000-binary-ops-specialization.md +++ b/text/0000-binary-ops-specialization.md @@ -23,7 +23,10 @@ data type (usually indicated by implementing the same `Borrow`), it makes sense to define binary operator trait impls that work between each pair of these types. However, with proliferation of special-purpose representations of widely used data types, such as byte arrays and strings, the number of -possible such pairs undergoes a combinatorial explosion. +possible such pairs undergoes a combinatorial explosion. Each of +the crates defining such special-purpose types has to be in a dependency +relationship with any other of these crates, otherwise there cannot be +an operator impl to make them work together. Specialization of blanket trait implementations could be used to deal with this problem. These two impls of `PartialEq` could automatically enable equality @@ -79,11 +82,18 @@ pub trait PartialEq2 { ## Default blanket implementation rule [default-blanket-implementation-rule]: #default-blanket-implementation-rule -The rule for any crate defining a type that needs to work as an argument type -in binary operators is to define default impls of the new-style operator -traits described in this RFC, where this type is the `&self` or `self` -operand type, and a generic type parameter bound by `Borrow` defines the -other operand's type: +Types that need to work as an operand type in binary operators broadly +fall into two categories. One category is use case specific representations +of an underlying data type that usually provides binary operators on itself. +For example, `String` is the owned version of `str`, and `PathBuf` is that +for `Path`. The underlying types themselves are counted in for the purposes +of the following rule. +These types usually implement `Borrow` to the basic type, and their Rust 1.0 +binary operator trait impls tend to cover any pairs with other types that +satisfy the same `Borrow`. The rule for the crate defining such a type is to +also define default impls of the new-style operator traits described in +this RFC, where this type is the `&self` or `self` operand type, and a +generic type parameter bound by `Borrow` defines the other operand's type: ```rust impl PartialEq2 for String @@ -97,11 +107,18 @@ where ``` The type parameter of the `Borrow` bound is the basic data type that `Self` -can also be borrowed as (which can always be just `Self`). The role of `Borrow` -therefore extends to stratifying operand types of binary operators +can also be borrowed as (which can be just `Self`). The role of `Borrow` +therefore extends to restricting operand types of binary operators available for the implementing type. -Notably, the standard library already maintains this stratification in the -provided implementations of `Borrow` and Rust 1.0 operator traits. +Notably, the standard library largely maintains this stratification in the +provided implementations of `Borrow` and Rust 1.0 operator traits; +`PathBuf`/`Path` is a [problematic][issue55319] exception. + +Other types do not have an underlying borrowable type to define their data +domain, but they still need cross-type operator compatibility between some +family of types. Examples from the standard library are `IpAddr`, `Ipv4Addr`, +and `Ipv6Addr`. These types can have their operator trait impls defined +in the old school non-generic way. ## Overload resolution [overload-resolution]: #overload-resolution @@ -128,6 +145,10 @@ old school operator traits. The proposed system allows legacy Rust 1.0 operator trait implementations to coexist with the new blanket implementations in a backward-compatible way. +Systematic application of the [default impl rule][default-blanket-implementation-rule] +can provide any-to-any operand type compatibility for all types sharing a +particular `Borrow` bound, without necessity for any two crates, each defining +one of these types, to be in a direct dependency relationship. Specialized implementations of new style traits can be defined when practical, within the `Borrow` bound of the default implementation. Crates can also choose to provide default (or even fully specialized blanket) impls of @@ -181,3 +202,4 @@ The circumstances that led to the design seem unique to Rust. Is it feasible to implement overload resolution in the compiler as proposed? [rfc1210]: ./1210-impl-specialization.md +[issue55319]: https://github.com/rust-lang/rust/issues/55319 From 707fb27105b1ae73f3ea0bf3720fead7fd1819a7 Mon Sep 17 00:00:00 2001 From: Mikhail Zabaluev Date: Thu, 25 Oct 2018 00:41:29 +0300 Subject: [PATCH 03/13] Drawback note on Borrow as the type bound --- text/0000-binary-ops-specialization.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/text/0000-binary-ops-specialization.md b/text/0000-binary-ops-specialization.md index 3f29ba2d3e9..f4602d5c15a 100644 --- a/text/0000-binary-ops-specialization.md +++ b/text/0000-binary-ops-specialization.md @@ -172,6 +172,10 @@ implementation errors. This takes further the precedent set by the [specialization RFC][rfc1210] that multiple different implementations may be considered to fit one use. +`Borrow` may prove to be too inflexible a bound for some interoperable +type families. It's a challenge, though, to come up with an example in +existing designs that goes beyond a few closely knit types. + # Rationale and alternatives [rationale-and-alternatives]: #rationale-and-alternatives From 9e169a162f4d9da332c0b9a4891b70939cdf53c5 Mon Sep 17 00:00:00 2001 From: Mikhail Zabaluev Date: Fri, 26 Oct 2018 15:25:52 +0300 Subject: [PATCH 04/13] Linkified the mention of specialization Link to the specialization RFC. --- text/0000-binary-ops-specialization.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/text/0000-binary-ops-specialization.md b/text/0000-binary-ops-specialization.md index f4602d5c15a..79c99926fe2 100644 --- a/text/0000-binary-ops-specialization.md +++ b/text/0000-binary-ops-specialization.md @@ -28,10 +28,10 @@ the crates defining such special-purpose types has to be in a dependency relationship with any other of these crates, otherwise there cannot be an operator impl to make them work together. -Specialization of blanket trait implementations could be used to deal with this -problem. These two impls of `PartialEq` could automatically enable equality -comparison for `String` on the left hand side and any type on the -right hand side that implements `Borrow`: +[Specialization][rfc1210] of blanket trait implementations could be used to +deal with this problem. These two impls of `PartialEq` could automatically +enable equality comparison for `String` on the left hand side and any type +on the right hand side that implements `Borrow`: ```rust impl PartialEq for String { From efd5451a10bdd3dabda4640cfb9ba004baba4175 Mon Sep 17 00:00:00 2001 From: Mikhail Zabaluev Date: Sat, 27 Oct 2018 04:05:16 +0300 Subject: [PATCH 05/13] Explain specifics of ownership-taking operators --- text/0000-binary-ops-specialization.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/text/0000-binary-ops-specialization.md b/text/0000-binary-ops-specialization.md index 79c99926fe2..03d4e35aead 100644 --- a/text/0000-binary-ops-specialization.md +++ b/text/0000-binary-ops-specialization.md @@ -114,6 +114,29 @@ Notably, the standard library largely maintains this stratification in the provided implementations of `Borrow` and Rust 1.0 operator traits; `PathBuf`/`Path` is a [problematic][issue55319] exception. +Operator traits that take ownership of the operands are trickier to implement +for non-`Copy` types: these should not work between two borrowed types +to avoid allocations hidden in operator notation, while consuming the +owned operands in an operator expression may be non-ergonomic. +A precedent is set in the `Add` implementation for `String` to only let +the left-hand operand value to be moved into the expression; the right-hand +side needs to be borrowed as a `str` reference. +To extend the operator's applicability to any types that satisfy +`Borrow`, the crate `std` defines this default implementation of +the new trait `Add2`: + +```rust +impl<'a, T> Add2<&'a T> for String +where T: Borrow +{ + type Output = String; + + default fn add2(self, other: &'a T) -> String { + self + other.borrow() + } +} +``` + Other types do not have an underlying borrowable type to define their data domain, but they still need cross-type operator compatibility between some family of types. Examples from the standard library are `IpAddr`, `Ipv4Addr`, From bfbe0f84025c8d9d3226ceae11623dafe78fa0d3 Mon Sep 17 00:00:00 2001 From: Mikhail Zabaluev Date: Sat, 4 May 2019 00:47:43 +0300 Subject: [PATCH 06/13] binary-ops-specialization: Proofreading Edited text to improve readablity. Added a `?Sized` bound in an example. --- text/0000-binary-ops-specialization.md | 43 +++++++++++++------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/text/0000-binary-ops-specialization.md b/text/0000-binary-ops-specialization.md index 03d4e35aead..a4dcc00fcb4 100644 --- a/text/0000-binary-ops-specialization.md +++ b/text/0000-binary-ops-specialization.md @@ -18,9 +18,10 @@ consider the new traits along with the old school non-specializable traits. [motivation]: #motivation Operator overloading brings a lot of convenience into usage of data types. -When a Rust type is one of multiple representations of the same underlying -data type (usually indicated by implementing the same `Borrow`), it makes -sense to define binary operator trait impls that work between each pair of +For a set of Rust types which provide different representations of the same +underlying data type (usually indicated by implementing the same `Borrow`), +it makes +sense to define binary operator trait impls that apply between each pair of these types. However, with proliferation of special-purpose representations of widely used data types, such as byte arrays and strings, the number of possible such pairs undergoes a combinatorial explosion. Each of @@ -52,7 +53,7 @@ where However, introducing default impls for already defined operator traits is a breaking change: there are crates that don't restrict their -binary operator type pairs to ones sharing the same `Borrow` target. +binary operators to types sharing the same `Borrow` target. One example is `bytes` defining `PartialEq` impls that allow comparing `Bytes` and the standard string types. While such data domain crossing is problematic for other reasons (e.g. differences in `Hash` for values that @@ -66,7 +67,7 @@ forbidden. New operator traits with blanket default impls abstracted over ## New operator traits [new-operator-traits]: #new-operator-traits -This proposal adds second-generation traits for all binary operators in +This proposal adds second-generation traits for all binary operators defined in the standard library where the right-hand operand type is defined generically. The traits are named `PartialEq2`, `PartialOrd2`, `Add2`, etc. and defined with the same method signatures as their Rust 1.0 counterparts. Example @@ -88,12 +89,12 @@ of an underlying data type that usually provides binary operators on itself. For example, `String` is the owned version of `str`, and `PathBuf` is that for `Path`. The underlying types themselves are counted in for the purposes of the following rule. -These types usually implement `Borrow` to the basic type, and their Rust 1.0 -binary operator trait impls tend to cover any pairs with other types that -satisfy the same `Borrow`. The rule for the crate defining such a type is to -also define default impls of the new-style operator traits described in -this RFC, where this type is the `&self` or `self` operand type, and a -generic type parameter bound by `Borrow` defines the other operand's type: +These types usually implement `Borrow` to the underlying type, and their +Rust 1.0 binary operator trait impls tend to cover any pairs with other types +that satisfy the same parameterized `Borrow`. The rule for a crate defining +such a type is to also define default impls of the new operator traits +described in this RFC, where a generic type parameter bound by `Borrow` +defines the operand type other than `Self`: ```rust impl PartialEq2 for String @@ -106,7 +107,7 @@ where } ``` -The type parameter of the `Borrow` bound is the basic data type that `Self` +The type parameter of the `Borrow` bound is the type that `Self` can also be borrowed as (which can be just `Self`). The role of `Borrow` therefore extends to restricting operand types of binary operators available for the implementing type. @@ -115,9 +116,9 @@ provided implementations of `Borrow` and Rust 1.0 operator traits; `PathBuf`/`Path` is a [problematic][issue55319] exception. Operator traits that take ownership of the operands are trickier to implement -for non-`Copy` types: these should not work between two borrowed types -to avoid allocations hidden in operator notation, while consuming the -owned operands in an operator expression may be non-ergonomic. +for non-`Copy` types: these should not work between two borrowed values +to avoid allocations hidden in operator notation, while moving the +owned operands into an operator expression may be non-ergonomic. A precedent is set in the `Add` implementation for `String` to only let the left-hand operand value to be moved into the expression; the right-hand side needs to be borrowed as a `str` reference. @@ -127,7 +128,7 @@ the new trait `Add2`: ```rust impl<'a, T> Add2<&'a T> for String -where T: Borrow +where T: ?Sized + Borrow { type Output = String; @@ -137,8 +138,8 @@ where T: Borrow } ``` -Other types do not have an underlying borrowable type to define their data -domain, but they still need cross-type operator compatibility between some +Other types do not have an underlying borrowable type indicating their data +domain, but they still need binary operators to apply across some family of types. Examples from the standard library are `IpAddr`, `Ipv4Addr`, and `Ipv6Addr`. These types can have their operator trait impls defined in the old school non-generic way. @@ -170,8 +171,8 @@ The proposed system allows legacy Rust 1.0 operator trait implementations to coexist with the new blanket implementations in a backward-compatible way. Systematic application of the [default impl rule][default-blanket-implementation-rule] can provide any-to-any operand type compatibility for all types sharing a -particular `Borrow` bound, without necessity for any two crates, each defining -one of these types, to be in a direct dependency relationship. +particular `Borrow` bound, without necessity for any two crates defining +these types to be in a direct dependency relationship. Specialized implementations of new style traits can be defined when practical, within the `Borrow` bound of the default implementation. Crates can also choose to provide default (or even fully specialized blanket) impls of @@ -195,7 +196,7 @@ implementation errors. This takes further the precedent set by the [specialization RFC][rfc1210] that multiple different implementations may be considered to fit one use. -`Borrow` may prove to be too inflexible a bound for some interoperable +`Borrow` may prove too inflexible a bound for some interoperable type families. It's a challenge, though, to come up with an example in existing designs that goes beyond a few closely knit types. From a8aa4cdb5fac6359120c615dca5d4a76e13b1830 Mon Sep 17 00:00:00 2001 From: Mikhail Zabaluev Date: Sat, 4 May 2019 14:48:37 +0300 Subject: [PATCH 07/13] Simplified the motivational example In binary-ops-specialization, the example does not need to provide a special case for string slices on the right hand side. --- text/0000-binary-ops-specialization.md | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/text/0000-binary-ops-specialization.md b/text/0000-binary-ops-specialization.md index a4dcc00fcb4..54d9457e7fd 100644 --- a/text/0000-binary-ops-specialization.md +++ b/text/0000-binary-ops-specialization.md @@ -30,23 +30,17 @@ relationship with any other of these crates, otherwise there cannot be an operator impl to make them work together. [Specialization][rfc1210] of blanket trait implementations could be used to -deal with this problem. These two impls of `PartialEq` could automatically +deal with this problem. This implementation of `PartialEq` could automatically enable equality comparison for `String` on the left hand side and any type on the right hand side that implements `Borrow`: ```rust -impl PartialEq for String { - fn eq(&self, other: &str) -> bool { - &self[..] == other - } -} - impl PartialEq for String where Rhs: ?Sized + Borrow, { default fn eq(&self, other: &Rhs) -> bool { - &self[..] == other.borrow() + self.as_str() == other.borrow() } } ``` From a3d72842c077d5e808a927b1b5339ceca5858510 Mon Sep 17 00:00:00 2001 From: Mikhail Zabaluev Date: Sat, 4 May 2019 14:53:12 +0300 Subject: [PATCH 08/13] Correct example definition of PartialEq2 The default body of method ne2 called an eq method that's not present in this trait. The second-gen method is named eq2. --- text/0000-binary-ops-specialization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0000-binary-ops-specialization.md b/text/0000-binary-ops-specialization.md index 54d9457e7fd..568e9808f02 100644 --- a/text/0000-binary-ops-specialization.md +++ b/text/0000-binary-ops-specialization.md @@ -70,7 +70,7 @@ for `PartialEq2`: ```rust pub trait PartialEq2 { fn eq2(&self, other: &Rhs) -> bool; - fn ne2(&self, other: &Rhs) -> bool { !self.eq(other) } + fn ne2(&self, other: &Rhs) -> bool { !self.eq2(other) } } ``` From 2af411a95e8579925c86f4c730ad11bf3c0919da Mon Sep 17 00:00:00 2001 From: Mikhail Zabaluev Date: Wed, 19 Jun 2019 05:56:18 +0300 Subject: [PATCH 09/13] binary_ops_specialization: the fallback rewrite Reinvented the specializable operator overload traits as fallbacks augmenting the extant operator traits rather than replacing them. --- text/0000-binary-ops-specialization.md | 231 ++++++++++++------------- 1 file changed, 114 insertions(+), 117 deletions(-) diff --git a/text/0000-binary-ops-specialization.md b/text/0000-binary-ops-specialization.md index 568e9808f02..0bc8f85b48a 100644 --- a/text/0000-binary-ops-specialization.md +++ b/text/0000-binary-ops-specialization.md @@ -6,30 +6,34 @@ # Summary [summary]: #summary -Provide new traits for overloading binary operators, such as -`PartialEq2`, `PartialOrd2`, `Add2`, `AddAssign2`, etc., -with default blanket implementations abstracted over `Borrow` -for built-in and standard library types. This will drastically reduce -the number of explicit impl items necessary to cover compatible type pairs. +Provide a family of traits to augment overloading of binary operators +with more practical generic implementations. The traits are named +`DefaultPartialEq`, `DefaultPartialOrd`, `DefaultAdd`, `DefaultAddAssign`, etc. +For these traits, default generic implementations abstracted over `Borrow` +are to be provided for built-in and standard library types, including +`[u8]`, `Vec`, `str`, `String`, `OsStr`, `OsString`, `Path`, `PathBuf`. +This will drastically reduce the number of explicit impl items necessary to +cover all type pairs on which the binary operators can act. + When resolving the implementation for an operator, the compiler will -consider the new traits along with the old school non-specializable traits. +consider the impls for the new "default" overload trait corresponding to the +operator as the second choice after its Rust 1.0 overload trait. # Motivation [motivation]: #motivation Operator overloading brings a lot of convenience into usage of data types. -For a set of Rust types which provide different representations of the same +For a set of types which provide different representations of the same underlying data type (usually indicated by implementing the same `Borrow`), -it makes -sense to define binary operator trait impls that apply between each pair of +it makes sense to define binary operator trait impls that act on each pair of these types. However, with proliferation of special-purpose representations -of widely used data types, such as byte arrays and strings, the number of -possible such pairs undergoes a combinatorial explosion. Each of -the crates defining such special-purpose types has to be in a dependency -relationship with any other of these crates, otherwise there cannot be +of widely used data types like byte arrays and strings, the number of +possible such pairs undergoes a quadratic explosion. Each of the crates +defining a type in the operator-compatible set has to be in a dependency +relationship with any other of such crates, otherwise there cannot be an operator impl to make them work together. -[Specialization][rfc1210] of blanket trait implementations could be used to +[Specialization][rfc1210] of trait implementations could be a convenient way to deal with this problem. This implementation of `PartialEq` could automatically enable equality comparison for `String` on the left hand side and any type on the right hand side that implements `Borrow`: @@ -45,88 +49,102 @@ where } ``` -However, introducing default impls for already defined operator traits -is a breaking change: there are crates that don't restrict their -binary operators to types sharing the same `Borrow` target. +However, introducing default impls for operator traits that have been +stable since Rust 1.0 is a breaking change: there are crates that don't +restrict their binary operators to types sharing the same `Borrow` target, +so their overload trait implementations will come into conflict. One example is `bytes` defining `PartialEq` impls that allow comparing `Bytes` and the standard string types. While such data domain crossing is problematic for other reasons (e.g. differences in `Hash` for values that compare as equal), the change should not break crates doing what has not been -forbidden. New operator traits with blanket default impls abstracted over -`Borrow` can provide a migration path and lay down discipline. +previously forbidden. Newly introduced fallback overload traits with generic +impls abstracted over `Borrow` provide a backward compatible solution +and lay down some discipline. # Guide-level explanation [guide-level-explanation]: #guide-level-explanation -## New operator traits -[new-operator-traits]: #new-operator-traits +## Default operator traits +[default-operator-traits]: #default-operator-traits + +This proposal adds secondary overload traits for all overloadable binary +operators where the right-hand operand type is generic. +The traits are named `DefaultPartialEq`, `DefaultPartialOrd`, `DefaultAdd`, etc. +and defined with the same type parameters and method signatures as the plain +old Rust 1.0 operator overload traits. -This proposal adds second-generation traits for all binary operators defined in -the standard library where the right-hand operand type is defined generically. -The traits are named `PartialEq2`, `PartialOrd2`, `Add2`, etc. and defined -with the same method signatures as their Rust 1.0 counterparts. Example -for `PartialEq2`: +Example for `DefaultPartialEq`: ```rust -pub trait PartialEq2 { - fn eq2(&self, other: &Rhs) -> bool; - fn ne2(&self, other: &Rhs) -> bool { !self.eq2(other) } +pub trait DefaultPartialEq { + fn eq(&self, other: &Rhs) -> bool; + fn ne(&self, other: &Rhs) -> bool { + !self.eq(other) + } } ``` -## Default blanket implementation rule -[default-blanket-implementation-rule]: #default-blanket-implementation-rule +## Default implementation rule +[default-implementation-rule]: #default-implementation-rule Types that need to work as an operand type in binary operators broadly -fall into two categories. One category is use case specific representations -of an underlying data type that usually provides binary operators on itself. -For example, `String` is the owned version of `str`, and `PathBuf` is that -for `Path`. The underlying types themselves are counted in for the purposes -of the following rule. -These types usually implement `Borrow` to the underlying type, and their -Rust 1.0 binary operator trait impls tend to cover any pairs with other types -that satisfy the same parameterized `Borrow`. The rule for a crate defining -such a type is to also define default impls of the new operator traits -described in this RFC, where a generic type parameter bound by `Borrow` -defines the operand type other than `Self`: +fall into two categories. One category is different purpose-specific +representations of an underlying data type that provides binary +operators acting on itself. We'll call it the **base operand type** for the +purposes of this proposal. +For example, `String` is the standard owned counterpart of `str`, +and `PathBuf` is this for `Path`. The base operand type itself is +considered together with its operand type family for the following rule. +Types in each such family usually implement `Borrow` to the base operand type, +and their binary operator trait impls, as currently provided, tend to cover +any possible pairs with the other types in the family. + +The rule for a crate defining such a type is to also define generic default +implementations of the default operator overload traits described in +this RFC, where a generic type parameter bound by `Borrow` to the base +operand type defines the operand type other than `Self`: ```rust -impl PartialEq2 for String +impl DefaultPartialEq for String where Rhs: ?Sized + Borrow, { - default fn eq2(&self, other: &Rhs) -> bool { + default fn eq(&self, other: &Rhs) -> bool { &self[..] == other.borrow() } } ``` -The type parameter of the `Borrow` bound is the type that `Self` -can also be borrowed as (which can be just `Self`). The role of `Borrow` -therefore extends to restricting operand types of binary operators -available for the implementing type. +The type parameter of the `Borrow` bound is the base operand type for `Self` +(which is `Self` in case the impl is defined for the base type itself). +The new semantic of `Borrow` therefore extends the "acts the same" +guarantee to binary operators available for the implementing type, which +is already the case for the intra-type traits `Eq`, `Ord`, and `Hash`. Notably, the standard library largely maintains this stratification in the -provided implementations of `Borrow` and Rust 1.0 operator traits; +provided implementations of `Borrow` and the plain old operator traits; `PathBuf`/`Path` is a [problematic][issue55319] exception. Operator traits that take ownership of the operands are trickier to implement -for non-`Copy` types: these should not work between two borrowed values -to avoid allocations hidden in operator notation, while moving the -owned operands into an operator expression may be non-ergonomic. +for non-`Copy` types: these should not work between two borrowed +values to avoid allocations or other side effects hidden in operator notation, +while moving both owned operands into an operator expression +may be non-ergonomic. A precedent is set in the `Add` implementation for `String` to only let -the left-hand operand value to be moved into the expression; the right-hand -side needs to be borrowed as a `str` reference. -To extend the operator's applicability to any types that satisfy -`Borrow`, the crate `std` defines this default implementation of -the new trait `Add2`: +the left hand operand value be moved into the expression, owing to the +left-associative order of evaluation; the right hand side needs to coerce +to an `str` reference. +`Deref` coercions go a long way to make pointers to various string types +fit that impl, but to extend the operator's applicability to any types +that satisfy `Borrow`, the crate `std` may provide this +default implementation of the new trait `DefaultAdd`: ```rust -impl<'a, T> Add2<&'a T> for String -where T: ?Sized + Borrow +impl<&'a, T> DefaultAdd<&'a T> for String +where T: Borrow { type Output = String; - default fn add2(self, other: &'a T) -> String { + default fn add(self, other: &'a T) -> String { self + other.borrow() } } @@ -135,93 +153,72 @@ where T: ?Sized + Borrow Other types do not have an underlying borrowable type indicating their data domain, but they still need binary operators to apply across some family of types. Examples from the standard library are `IpAddr`, `Ipv4Addr`, -and `Ipv6Addr`. These types can have their operator trait impls defined -in the old school non-generic way. +and `Ipv6Addr`. These types can have their plain old operator trait impls +defined just like they do now. ## Overload resolution [overload-resolution]: #overload-resolution -When picking the implementation for an operator backed by the proverbial -traits `Op` and `Op2`, the compiler will consider the available trait -implementations in the following order: - -1. Fully specialized impls of `Op2`; -2. Fully specialized impls of `Op`; -3. Default impls of `Op2`; -4. Default impls of `Op`. - -## Path for future migration -[path-for-future-migration]: #path-for-future-migration - -The Rust 1.0 binary operator traits can(?) be deprecated -after the new traits are introduced. In the future backward-incompatible -Rust 2.0, the new traits will lose their `2` name suffix and replace the -old school operator traits. +When picking the implementation for an operator overloaded by the notional +traits `Op` and `DefaultOp`, the compiler will consider the available +implementations for `Op` first, before falling back to `DefaultOp`. # Reference-level explanation [reference-level-explanation]: #reference-level-explanation -The proposed system allows legacy Rust 1.0 operator trait implementations -to coexist with the new blanket implementations in a backward-compatible way. -Systematic application of the [default impl rule][default-blanket-implementation-rule] +The proposed system allows the existing operator trait implementations +to coexist with the newly introduced generic default overload trait +implementations in a complementary and backward compatible way. +Systematic application of the [default impl rule][default-implementation-rule] can provide any-to-any operand type compatibility for all types sharing a particular `Borrow` bound, without necessity for any two crates defining these types to be in a direct dependency relationship. -Specialized implementations of new style traits can be defined when practical, -within the `Borrow` bound of the default implementation. Crates can also -choose to provide default (or even fully specialized blanket) impls of -the legacy traits, but new-style impls should be preferred in new APIs. - -The interleaved, specialized-first overload resolution rule is designed to -prevent "spooky action at a distance" where e.g. adding a blanket impl of -`Add2` for type `A` defined in one crate could shadow an existing -`impl Add for A` in another crate that defines `B`. The situation where -non-generic impls of `Add` and `Add2`, defined in different crates, could -apply to the same pair of types, is impossible due to acyclicity of crate -dependencies and the orphan rule. + +Specialized implementations of the default overload traits can be defined +when practical, within the `Borrow` bound of the default implementation. +Crate authors are also free to provide new impls of the plain old overload +traits, which override the generic impls of the default overload traits +for purposes of operator overloading, or apply outside of the type families +circumscribed by the default overload trait impls. # Drawbacks [drawbacks]: #drawbacks -The second-generation traits add complexity, especially to operator -overload resolution. It's likely that both new and old school trait impls -will have to be provided side by side, which increases the possibility of +The fallback overload traits add complexity, especially to operator +overload resolution. It's likely that implementations for both default and +plain old operator traits will have to be provided side by side to support +older versions of the compiler, which increases the possibility of implementation errors. This takes further the precedent set by the [specialization RFC][rfc1210] that multiple different implementations may be considered to fit one use. -`Borrow` may prove too inflexible a bound for some interoperable -type families. It's a challenge, though, to come up with an example in -existing designs that goes beyond a few closely knit types. - # Rationale and alternatives [rationale-and-alternatives]: #rationale-and-alternatives -The proposed [rule][default-blanket-implementation-rule] of defining -operator impls slashes the combinatorial explosion of mostly tedious -operator trait implementations seen today, leaving reasonable flexibility -in which operand type pairs are allowed (with `Borrow` as the guiding force). - -Addition of second-generation traits on top of the existing system -provides a backward-compatible migration path for the Rust 1.x timeframe. - -If opt-in feature gates were possible in the stable channel, the new default -impls could be defined for the Rust 1.0 traits and hidden behind a feature -gate. It's unclear to the author if this could work without the need for -all crates in the dependency graph to be compatible with the feature. +The proposed [rule][default-implementation-rule] of defining generic +operator impls slashes the quadratic explosion of mostly tedious +non-generic operator trait implementations that takes place today. +The crate authors are free to define non-generic impls of plain old operator +traits as they see fit, including outside of the `Borrow` type family +of the default generic impl. + +Previous revisions of this RFC envisioned the new traits as replacements +for the Rust 1.0 operator traits, which would be soft-deprecated. This +limited the space for any new custom overload impls to specializations of the +`Borrow` bound of the default generic impl, and complicated the rules +for overload resolution in order to avoid "spooky action at a distance", +when adding generic impls of new style operator traits could shadow old style +concrete impls defined in a different crate. # Prior art [prior-art]: #prior-art -Labeling second-generation APIs with suffix `2` to allow coexistence -with the legacy APIs is common. - -The circumstances that led to the design seem unique to Rust. +The language evolution that led to this design seems unique to Rust. # Unresolved questions [unresolved-questions]: #unresolved-questions -Is it feasible to implement overload resolution in the compiler as proposed? +None. [rfc1210]: ./1210-impl-specialization.md [issue55319]: https://github.com/rust-lang/rust/issues/55319 From 8d57d0f7124723d866a995fc7a1e8936912c4728 Mon Sep 17 00:00:00 2001 From: Mikhail Zabaluev Date: Wed, 19 Jun 2019 09:07:47 +0300 Subject: [PATCH 10/13] Move prose on overload resolution to the reference section --- text/0000-binary-ops-specialization.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/text/0000-binary-ops-specialization.md b/text/0000-binary-ops-specialization.md index 0bc8f85b48a..5930f0f2dc8 100644 --- a/text/0000-binary-ops-specialization.md +++ b/text/0000-binary-ops-specialization.md @@ -156,16 +156,13 @@ family of types. Examples from the standard library are `IpAddr`, `Ipv4Addr`, and `Ipv6Addr`. These types can have their plain old operator trait impls defined just like they do now. -## Overload resolution -[overload-resolution]: #overload-resolution +# Reference-level explanation +[reference-level-explanation]: #reference-level-explanation When picking the implementation for an operator overloaded by the notional traits `Op` and `DefaultOp`, the compiler will consider the available implementations for `Op` first, before falling back to `DefaultOp`. -# Reference-level explanation -[reference-level-explanation]: #reference-level-explanation - The proposed system allows the existing operator trait implementations to coexist with the newly introduced generic default overload trait implementations in a complementary and backward compatible way. From 4936b7099106b118a84c9b7a6fd4206340d477ed Mon Sep 17 00:00:00 2001 From: Mikhail Zabaluev Date: Wed, 19 Jun 2019 09:25:18 +0300 Subject: [PATCH 11/13] Fix a mind slip in an example --- text/0000-binary-ops-specialization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0000-binary-ops-specialization.md b/text/0000-binary-ops-specialization.md index 5930f0f2dc8..1592ef8b85c 100644 --- a/text/0000-binary-ops-specialization.md +++ b/text/0000-binary-ops-specialization.md @@ -139,7 +139,7 @@ that satisfy `Borrow`, the crate `std` may provide this default implementation of the new trait `DefaultAdd`: ```rust -impl<&'a, T> DefaultAdd<&'a T> for String +impl<'a, T> DefaultAdd<&'a T> for String where T: Borrow { type Output = String; From cb3ff4f517c5632eb3e6fadb5ae56ddc7f795a69 Mon Sep 17 00:00:00 2001 From: Mikhail Zabaluev Date: Fri, 29 May 2020 19:19:17 +0300 Subject: [PATCH 12/13] Added the PR link --- text/0000-binary-ops-specialization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0000-binary-ops-specialization.md b/text/0000-binary-ops-specialization.md index 1592ef8b85c..669aa5f2f72 100644 --- a/text/0000-binary-ops-specialization.md +++ b/text/0000-binary-ops-specialization.md @@ -1,6 +1,6 @@ - Feature Name: binary_ops_specialization - Start Date: 2018-10-20 -- RFC PR: +- RFC PR: [rust-lang/rfcs#2578](https://github.com/rust-lang/rfcs/pull/2578) - Rust Issue: # Summary From 429568d0edcf58e3b1c059a4a7cdb5f100fac317 Mon Sep 17 00:00:00 2001 From: Mikhail Zabaluev Date: Fri, 29 May 2020 19:41:01 +0300 Subject: [PATCH 13/13] Refined language on the motivational part --- text/0000-binary-ops-specialization.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/text/0000-binary-ops-specialization.md b/text/0000-binary-ops-specialization.md index 669aa5f2f72..043db241863 100644 --- a/text/0000-binary-ops-specialization.md +++ b/text/0000-binary-ops-specialization.md @@ -22,9 +22,10 @@ operator as the second choice after its Rust 1.0 overload trait. # Motivation [motivation]: #motivation -Operator overloading brings a lot of convenience into usage of data types. -For a set of types which provide different representations of the same -underlying data type (usually indicated by implementing the same `Borrow`), +Operator overloading makes data types more convenient to use. +For a set of types which provide different containers and ownership patterns +for the same underlying data type (usually indicated by implementing the same +`Borrow`), it makes sense to define binary operator trait impls that act on each pair of these types. However, with proliferation of special-purpose representations of widely used data types like byte arrays and strings, the number of