default impl
does not really mean what one may think. You may be surprised to know that:
#![feature(specialization)]
default impl<T : ?Sized> Foo for T {}
trait Foo {}
does not make all the types be Foo
: Playground
Instead, default impl
is there to provide default items / to make implementors of the trait be allowed to skip some definitions.
The best example where that is useful is when dealing with trait objects while offering ergonomics:
Click to show an example
Imagine having:
trait Rng
where
Self : 'static, // let's not bring non-`'static` lifetimes to this example π
{
fn next (&mut self) -> f64;
}
that you want to use as a trait object (e.g., to be able to dynamically override the rng generation), and yet you also want to be able to "clone" at some point for reproductible rng runs (e.g., for testing): &dyn Rng -> Box<dyn Rng>
:
trait ClonableRng
where
Self : 'static, // let's not bring non-`'static` lifetimes to this example π
{
fn next (&mut self) -> f64;
fn clone_boxed (self: &'_ Self) -> Box<dyn ClonableRng>
{
// Wops, this requires `Self : Sized + Clone`
Box::new(self.clone())
}
// what about...
fn clone_boxed (self: &'_ Self) -> Box<dyn ClonableRng>
where
// Wops, this is now unusable by a trait object
Self : Sized + Clone,
{
Box::new(self.clone())
}
}
The only solution is to provide Box::new(slef.clone())
in the context of self
not being a trait object, i.e., when impl
-ementing the trait for a concrete (or at least Sized + Clone
) type!
trait ClonableRng
where
Self : 'static, // let's not bring non-`'static` lifetimes to this example π
{
fn next (&mut self) -> f64;
fn clone_boxed (self: &'_ Self) -> Box<dyn ClonableRng>
; // no default impl
}
// Dowstream user:
impl Clone for MySuperDuperType { ... } // or #[derive(Clone)]
impl ClonableRng for MySuperDuperType {
fn next (&mut self) -> f64;
// damn, having to provide this for each implementor is
// distracting, repetitive, and thus annoying!
fn clone_boxed (self: &'_ Self) -> Box<dyn ClonableRng>
{
Box::new(self.clone())
}
}
to solve this in stable Rust one needs to go through some extra hoops (mainly an extra trait):
trait ClonableRng : CloneDynClonableRng
where
Self : 'static, // let's not bring non-`'static` lifetimes to this example π
{
fn next (&mut self) -> f64;
}
// where
trait CloneDynClonableRng {
fn clone_boxed (self: &'_ Self) -> Box<dyn ClonableRng>
; // no default impl here
}
// Default generic impl with the correct bounds!
impl<T : Clone> CloneDynClonableRng for T
where
T : ClonableRng, // Oh yes, we also need this bound
{
fn clone_boxed (self: &'_ Self) -> Box<dyn ClonableRng>
{
Box::new(
self.clone() // Thanks to Clone
) // coercion to dyn ClonableRng thanks to the ClonabelRng bound
}
}
Needless, to say, this leads to a less readable API (extra trait), and not everyone knows this workaround.
This is where the default impl
shines! Indeed, you get to add bounds for a default impl
without restricting the "official" API of the method (e.g., to keep it being object-safe):
#![feature(specialization)]
trait ClonableRng
where
Self : 'static, // let's not bring non-`'static` lifetimes to this example π
{
fn next (&mut self) -> f64;
fn clone_boxed (self: &'_ Self) -> Box<dyn ClonableRng>
; // no default impl here
}
// Default generic impl with the correct bounds!
default
impl<T : Clone> ClonableRng for T {
fn clone_boxed (self: &'_ Self) -> Box<dyn ClonableRng>
{
Box::new(
self.clone() // Thanks to Clone
) // coercion to dyn ClonableRng thanks to the (implicit!) ClonableRng bound
}
}
This reads well and is intuitive.
- As you can see,
default impl
's purpose is to have partial implementations. It has to be understood as partial impl
: the impl
only takes place once there is an actual non-default
impl
that completes it.
What you are looking for is rather something liek:
#![feature(imaginary_syntax)]
pub trait Serialize {}
pub trait Unwrapper {
type Inner: Serialize;
fn unwrap(&self) -> &Self::Inner;
}
impl<T> Unwrapper for T
where
T: Serialize,
{
default {
// Here is a default impl, with two items that are tied together:
type Inner = T;
fn unwrap(&self) -> &Self::Inner {
self
}
}
}
At first glance it looks like it could work, but the whole semantics of default { ... }
blocks would need to be clarified to know if it is really the case.
For instance, the default
block would lead to:
fn foo<T : Unwrapper> (it: T) {
let unwrapped: &T = it.unwrap(); // Error, &T vs &<T as Unwrapper>::Inner
}
which is not a probelm per se, just omething to be aware of.
Now, these default
blocks could be skipped altogether thanks to existential types (type_alias_impl_trait
):
#![feature(type_alias_impl_trait, specialization)]
pub
trait Serialize {}
pub
trait Unwrapper {
type Inner: Serialize;
fn unwrap(&self) -> &Self::Inner;
}
impl<T> Unwrapper for T
where
T : Serialize + 'static,
{
// For each `impl` hide the actual type except for the
// knowledge of it being `'static` and implementing `Serialize`.
type Inner = impl 'static + Serialize;
// Here is one overrideable method that makes `Inner` resolve
// to `T` before getting abstracted away into `impl Serialize`
default
fn unwrap(&self) -> &Self::Inner {
self
}
}
struct Wrapper<T>(T);
impl<T> Unwrapper for Wrapper<T>
where
T : Serialize + 'static,
{
// For each `impl` hide the actual type except for the
// knowledge of it being `'static` and implementing `Serialize`.
type Inner = impl 'static + Serialize;
// Here is one overrideable method that makes `Inner` resolve
// to `T` before getting abstracted away into `impl Serialize`
fn unwrap(&self) -> &Self::Inner {
&self.0
}
}
And by annotating / infecting Unwrapper
with a generic lifetime parameter one can get rid of 'static
: Playground
And maybe in the future this could be simply written as:
#![feature(imaginary_future)]
pub
trait Unwrapper {
fn unwrap(&self) -> &impl Serialize;
}
impl<T : Serialize> Unwrapper for T {
default
fn unwrap(&self) -> &impl Serialize {
self
}
}
struct Wrapper<T>(T);
impl<T : Serialize> Unwrapper for Wrapper<T> {
fn unwrap(&self) -> &impl Serialize {
&self.0
}
}