When writing trait definitions, I sometimes need to specify methods that have an obvious implementation for most, but not all, types that implement the trait. The most recent example comes from my answer to this thread on URLO, where I needed to specify parallel methods: One that has a receiver type of Self (where Self: Sized) and another that has a receiver of type Box<Self> (to allow usage by trait objects).
Ideally, I'd like to provide a default implementation of the boxed version that's only valid for Sized types, but this isn't possible today. Would something along these lines be reasonable to add to Rust?:
trait Consume {
fn consume(self) where Self: Sized;
fn consume_boxed(self: Box<Self>) default where Self:Sized { // example syntax
Self::consume(*self)
}
}
This syntax¹ (default where) is meant to indicate the bounds required for the default implementation to function. This is distinct from a normal where clause, which describes the bounds that must be fulfilled for the method to be called; both clauses may be present on the same method.
In this case, all types that implement the Consume trait must have a valid consume_boxed implementation (because there's no ordinary where clause). Sized types can use the default implementation, but !Sized types must override consume_boxed within the corresponding impl Consume block.
Like the unbounded default methods Rust currently has, any impl Consume block may provide its own implementation of consume_boxed. This is necessary to maintain forward compatibility: The default where bound could become satisfied if a mentioned type adds a trait implementation, which isn't supposed to be a breaking change.
¹ I haven't thought through the particular syntax much; that can be bikeshedded if the community decides this is a worthwhile capability to add.
I think this can be done with specialization and by splitting it into multiple traits:
#![feature(specialization)]
trait Consume {
// this needs to have a default implementation or you can't impl Consume for T where T: ?Sized,
fn consume(self)
where
Self: Sized
{
// a bit better, @2e71828 below
unimplemented!()
}
}
trait ConsumeBoxed: Consume {
fn consume_boxed(self: Box<Self>);
}
impl<T: Consume> ConsumeBoxed for T {
// default implementation if T: Sized
default fn consume_boxed(self: Box<Self>) {
Self::consume(*self);
}
}
impl Consume for str {}
impl ConsumeBoxed for str {
fn consume_boxed(self: Box<Self>) {
// do something
}
}
Additionally, the no-op default for consume() is unfortunate, as the compiler will no longer complain if you forget to implement it.
Using trait objects is also cumbersome, as you need to remember that Box<T:Consume> should be coerced into Box<dyn ConsumeBoxed> instead of Box<dyn Consume>.
The fact that Rust currently complains about missing methods when the method has Self: Sized and the implementor is unconditionally unsized is a separate concern. But it has the effect that implementing this trait as-is for str is kind-of impossible, unless I’m missing something xD
error[E0119]: conflicting implementations of trait `Consume` for type `S<_>`
--> src/lib.rs:20:1
|
12 | default impl<T> Consume for T {
| ----------------------------- first implementation here
...
20 | impl<T: ?Sized> Consume for S<T> {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `S<_>`
error: aborting due to previous error; 1 warning emitted
I guess that this needs improvement. Makes me wonder what the effect of a default impl is if it specializes a non-default impl? ………
A few tests later… Okay, it doesn’t specialize anything effectively, e.g. see this playground (where actual specialization only happens with an ordinary impl, as can be seen by the runtime behavior if you click Run). But it still errors in the same cases as ordinary impl does, e.g. in this playground where the default was removed from the fn consume_boxed in impl<T: ?Sized> Consume for S<T>.
I believe default impl allows you to implement only part of all required methods of a trait and then allow later impl blocks to implement the rest of the methods to end up with a complete implementation of the trait.
The specialization design in this RFC also allows for default impls , which can provide specialized defaults without actually providing a full trait implementation:
[...]
This default impl does not mean that Add is implemented for all Clone data, but just that when you do impl Add and Self: Clone , you can leave off add_assign :
[...]
This is accomplishable with just min_specialization, a split trait, and no dummy impls: [playground]
#![feature(min_specialization)]
trait Consume: ConsumeBoxed + Sized {
fn consume(self);
}
trait ConsumeBoxed {
fn consume_boxed(self: Box<Self>);
}
impl<T> ConsumeBoxed for T
where
T: Consume,
{
default fn consume_boxed(self: Box<Self>) {
dbg!("default consume_boxed");
Self::consume(*self)
}
}
impl Consume for i32 {
fn consume(self) {
dbg!("i32::consume");
dbg!(self);
}
}
impl Consume for String {
fn consume(self) {
dbg!("String::consume");
dbg!(self);
}
}
impl ConsumeBoxed for String {
fn consume_boxed(self: Box<Self>) {
dbg!("String::consume_boxed");
dbg!(self);
}
}
impl ConsumeBoxed for str {
fn consume_boxed(self: Box<Self>) {
dbg!("str::consume_boxed");
dbg!(self);
}
}
fn main() {
let s: String = "owned".into();
s.consume();
let s: Box<str> = "boxed".to_string().into_boxed_str();
s.consume_boxed();
let s: Box<String> = Box::new("double indirection".into());
s.consume_boxed();
let i: i32 = 5;
i.consume();
let i: Box<i32> = Box::new(5000);
i.consume_boxed();
}
With full specialization, being able to use only one trait with a partial specialized default is definitely nice, however. [playground]
#![feature(trivial_bounds)]
#![feature(specialization)]
trait Consume {
fn consume(self)
where
Self: Sized;
fn consume_boxed(self: Box<Self>);
}
default impl<T> Consume for T {
default fn consume_boxed(self: Box<Self>) {
dbg!("default consume_boxed");
Self::consume(*self)
}
}
impl Consume for i32 {
fn consume(self) {
dbg!("i32::consume");
dbg!(self);
}
}
impl Consume for String {
fn consume(self) {
dbg!("String::consume");
dbg!(self);
}
fn consume_boxed(self: Box<Self>) {
dbg!("String::consume_boxed");
dbg!(self);
}
}
impl Consume for str {
fn consume(self)
where
str: Sized,
{
unreachable!("type system contradiction")
}
fn consume_boxed(self: Box<Self>) {
dbg!("str::consume_boxed");
dbg!(self);
}
}
fn main() {
let s: String = "owned".into();
s.consume();
let s: Box<str> = "boxed".to_string().into_boxed_str();
s.consume_boxed();
let s: Box<String> = Box::new("double indirection".into());
s.consume_boxed();
let i: i32 = 5;
i.consume();
let i: Box<i32> = Box::new(5000);
i.consume_boxed();
}
The point of this is mostly to say that default where isn't really necessary, because it's pretty much isomorphic to the primary use case (and contains the same fallout issues as) min_specialization, so sound specialization would both be a prerequisite to sound default where and make default where mostly unnecessary.
Default function method implementations are after all almost just sugar for a blanket (partial) specialization. (Exception: all methods provided.)
Indeed, I noticed that too, the error message if you remove it mentions it. I’ve had to think a bit about why I’ve never seen that message in the past. I got the answer now: Last time I hit this problem I’ve tried something like implementing this trait
trait Foo {
fn method(self) where Self: Sized;
}
but of course, without specialization, I was on stable and implementing this trait for str on stable only gives the way more subtle
= help: see issue #48214
The approaches I tried at the time were probably these
impl Foo for str {
fn method(self) where Self: Sized {}
}
error[E0277]: the size for values of type `str` cannot be known at compilation time
--> src/lib.rs:7:5
|
7 | fn method(self) where Self: Sized {}
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time
|
= help: the trait `Sized` is not implemented for `str`
= help: see issue #48214
error: aborting due to previous error
and
impl Foo for str {
fn method(self) {}
}
error[E0277]: the size for values of type `str` cannot be known at compilation time
--> src/lib.rs:7:15
|
7 | fn method(self) {}
| ^^^^ doesn't have a size known at compile-time
|
= help: the trait `Sized` is not implemented for `str`
help: function arguments must have a statically known size, borrowed types always have a known size
|
7 | fn method(&self) {}
| ^
error: aborting due to previous error
and
impl Foo for str {}
error[E0046]: not all trait items implemented, missing: `method`
--> src/lib.rs:6:1
|
2 | fn method(self) where Self: Sized;
| ---------------------------------- `method` from trait
...
6 | impl Foo for str {}
| ^^^^^^^^^^^^^^^^ missing `method` in implementation
error: aborting due to previous error
Spotting the #48214 amongst all the errors and figuring out that it’s supposed to be about a feature that removes the error instead of just providing explanation on the error itself is the hard part.
I, too, have had to sort of learn from experience that the compiler will only tell me about features on nightly. It'd be nice if there was some clue to at least check this (though I'd also understand concerns about seeming to imply a feature is coming to stable at all or any time soon).