Hello everyone,
I wanted to share an idea I had about a formulation for specialization that wouldn't rely on overlapping impls. Given how much work has been put into specialization and how advanced design and implementation are, I don't believe this alternative formulation should be considered now, but I wanted to share it in the hope it would be useful nevertheless.
So, currently specialization relies on having overlapping impl
s where a "more specific" one can be chosen. In this alternative, there is no "more specifc" impl
s, and overlapping impl
s always result in an error like in today's Rust.
Rather, it allows to specify several distinct implementations when declaring an impl<T>
, so that the correct implementation is chosen depending on the bounds on T
or its concrete value. To do so, we allow a new syntactical construct, where match
that allows matching on "type patterns".
For an example, let's consider the AddAssign
trait from the specialization RFC:
trait AddAssign<Rhs=Self> {
fn add_assign(&mut self, rhs: Rhs);
}
And the various specialized impl
s, using where match
:
impl<R, T> AddAssign<R> for T
where match T {
T: AddAssignSpec<R> => {
fn add_assign(&mut self, rhs: R) {
self.add_assign(rhs);
}
}
T: Add<R> + Copy => {
fn add_assign(&mut self, rhs: R) {
*self = *self + tmp;
}
}
T: Add<R> + Clone => {
fn add_assign(&mut self, rhs: R) {
let tmp = self.clone() + rhs;
*self = tmp;
}
}
}
In this example, we where match
on the T
generic parameter, which allows us to choose an implementation of the AddAssign
trait depending on the bounds on T
.
In each "arm" of the match
, we have a classic pattern => impl
, where pattern
is a new kind of pattern, a "type pattern", and impl
is the contents of the item being defined (here, the contents of the trait implementation).
The T
type is evaluated against each pattern in each arm, and the selected implementation is the one of the first arm whose pattern matches T
.
So, for instance, in the example above, if T
implements the AddAssignSpec<R>
trait, then the first implementation, that actually delegates to this AddAssignSpec<R>
trait, is selected. This allows for specialization, as a type that wishes to specialize its behavior with regard to AddAssign
just needs to implement AddAssignSpec<R>
, and this impl will be chosen even if the type is Clone
or Copy
. The AddAssignSpec<R>
trait is a regular, good old trait that requires implementing fn add_assign(&mut self, rhs: R)
.
If the type does not implements AddAssignSpec<R>
, the next pattern is evaluated, and so the impl is chosen if the type is Add<R> + Copy
. If the type is not Add<R> + Copy
, the third implementation is chosen if the type is Add<R> + Clone
. Finally, if the type is none of the above, pattern matching fails, and compilation fails.
Now, I did not try to formalize what kinds of type patterns would be allowed for matching, but I guess a first list would be:
- A concrete type, e.g.
u32
- A free identifier name (in the example above,
T
) with or without bounds (T
alone matches anything). -
_
(matches anything, not very different fromT
without bounds, but cannot appear on the right side) - Lifetimes appearing in types would not be allowed to be different from
'_
(or elided), as matching on the lifetime would be error-prone (and I heard it might be hard on codegen too)
A where match
clause could appear in trait implementations, in functions definitions or in trait definition.
For example, the example above could be rewritten more concisely as:
impl<R, T> AddAssign<R> for T {
fn add_assign(&mut self, rhs: R) where match T {
T: AddAssignSpec<R> => self.add_assign(rhs),
T: Add<R> + Copy => *self = *self + rhs,
T: Add<R> + Clone => { let tmp = self.clone() + rhs; *self = tmp; }
}
}
The second motivational example in the RFC is that of the Extend
trait, and could be rewritten as follows:
pub trait Extend<A> {
fn extend<T>(&mut self, iterable: T) where T: IntoIterator<Item=A>;
}
impl<A> Extend<A> for Vec<A> {
fn extend<T>(&mut self, iterable: T) where match T {
T: ExtendSpec<Vec<A>> => iterable.extend(&mut self),
&'_ [A] => { /* optimized implementation */ }
T: TrustedLen => { self.reserve(iterable.size_hint().1.unwrap()); for x in iterable { self.push(x) } }
T: IntoIterator<Item=A> => for x in iterable { self.push(x) }
}
}
Again, a type can specifically opt-in specialization by implementing the trait ExtendSpec<Vec<A>>
. Otherwise, if it isn't done, then if the type is &'_ [A]
, we can implement an optimized implementation (that the RFC doesn't provide and I'm too lazy to implement myself). Importantly, in this block, the iterable
variable is known to be of type &'_ [A]
, which allows to use its methods etc. etc.
Otherwise, we have yet another impl if T is TrustedLen
, and finally the "default" impl if T is IntoIterator<Item=A>
.
The specialization RFC allows to define "default partial trait implementations" for some types. This is possible with where match
clauses, by allowing them in trait definitions.
Using again the example from the specialization RFC:
trait Add<Rhs=Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
fn add_assign(&mut self, rhs: Rhs) where match Self {
T : Copy => { *self = *self + rhs; }
T : Clone => { let tmp = self.clone() + rhs; *self = tmp; }
_ => impl;
}
}
}
In this example, we provide some default implementations for add_assign
, under some conditions.
If the type is Copy
, we provide the first implementation as default implementation. This doesn't force a Copy type to use this implementation, though, they can still manually redefine add_assign
in their trait implementation.
If the type is not Copy
nor Clone
, we use the _ => impl
arm, where the impl
keyword indicates that there is no default implementation provided and that, while types are allowed to implement this type, they should provide an implementation of the add_assign
function. It is important not to forget that arm, as otherwise only Copy
or Clone
types would be able to implement the add_assign
function.
So, what do you think? Has something like this already been suggested before (I couldn't find anything with a cursory search in internals)? Certainly there are 1000s of reasons why this design would not work in practice, but I thought the idea was cool, as I really like matching on values in rust, it is a very expressive construct, and so I thought it would be nice to be able to do the same on types.
As I said, I don't expect this to influence the language in any capacity, but feel free to discuss!