While updating my dyn Trait
tour to note that RPITIT stabilized, I ran across another change that had flown under my radar:[1] as of Rust 1.72, you can opt associated types and GATs out of dyn
-usability. This is a mini-tutorial/exploration about that.[2]
Opting out with Self: Sized
If you're very aware of dyn Trait
, you're probably aware that the mechanism to opt out of a method being dyn
-dispatchable is to put a Self: Sized
bound on the method.
trait WishThisTraitWasDynSafe {
fn foo(&self);
// Methods with type generics are not `dyn`-safe
fn bar<T>(&self, _: T);
}
trait ThisTraitIsDynSafe {
fn foo(&self);
// The `Self: Sized` bound makes it unusable by `dyn _`
// (and any other `Sized` implementor) and thus this
// trait as a whole is `dyn`-safe (but this method is
// not `dyn`-dispatchable; you can't call it from `dyn _`).
fn bar<T>(&self, _: T) where Self: Sized;
}
GATs also make a trait non-dyn
-safe...
trait AddedGatAndLostDynSafe {
type Gat<T>; // :-(
fn foo(&self);
fn bar<T>(&self, _: T) where Self: Sized;
}
...but with the new feature, you can make the trait dyn
-safe again by opting GATs out of dyn
-usability in the same manner:
trait OptOutOfDynUsability {
type Gat<T> where Self: Sized;
fn foo(&self);
fn bar<T>(&self, _: T) where Self: Sized;
}
fn _dyn_safe_again_and_do_not_need_to_specify_gat_equality() {
// No `Trait<Gat = ..>` required!
let _: &dyn OptOutOfDynUsability = &();
}
This does mean that any methods utilizing the GAT can't be dyn
-dispatchable, though.[3]
trait OptOutWithMethod {
type Gat<T> where Self: Sized;
fn foo(&self);
fn bar<T>(&self, _: T) where Self: Sized;
fn quz<T>(&self, _: T) -> Self::Gat<T>
where
// This is required due to the bound on the GAT
Self: Sized,
;
}
It works for non-generic associated types too
When GATs were stabilized in 1.65, we got the ability to add where
clauses to non-generic associated types instead of just bounds on the type directly.[4] And that applies to this dyn
-usability opt-out too. This means you can have a dyn
-safe trait with an associated type, and not have to mention the associated type in the dyn Trait
!
trait AssocOptOut {
// We can opt `Foo` out of `dyn`-usability
type Foo where Self: Sized;
// But we have to opt out any methods that use it
// from `dyn`-dispatchability
fn foo(&self) -> Self::Foo where Self: Sized;
}
impl AssocOptOut for i32 {
type Foo = ();
fn foo(&self) -> Self::Foo {}
}
impl AssocOptOut for u64 {
type Foo = f32;
fn foo(&self) -> Self::Foo { 0.0 }
}
Which also means you can erase base types that define different associated types to the same dyn Trait
type.
fn make_use_of_assoc_opt_out() {
// No need for `dyn AssocOptOut<Foo = ()>`!
let mut a: &dyn AssocOptOut = &0_i32;
// No need for associated type equality between base types!
a = &0_u64;
// This fails because the type is not defined
// (`dyn AssocOptOut` is not `Sized`)
// let _: <dyn AssocOptOut as AssocOptOut>::Foo = todo!();
}
And the main limitation again is that you lose dyn
-dispatchability on any methods that use the associated type.
You can still specify the associated type if you want
You can still specify a non-dyn
-usable associated type in dyn Trait
if you want. However, just like things worked before this feature, this results in different dyn Trait<_ = ...>
types that differ from each other when the specified associated types differ.
pub fn but_i_want_a_specific_foo_grumpy_face() {
let mut a: &dyn AssocOptOut<Foo = ()> = &0_i32;
// This fails because the associate types don't match
// (`u64` can't coerce to a `dyn AssocOptOut<Foo = ()>`)
a = &0_u64;
// You can't just ditch the associated type equality either
let b: &dyn AssocOptOut = a;
}
This does result in a warning:
warning: unnecessary associated type bound for not object safe associated type
--> src/lib.rs:96:33
|
96 | let mut a: &dyn AssocOptOut<Foo = ()> = &0_i32;
| ^^^^^^^^ help: remove this bound
|
= note: this associated type has a `where Self: Sized` bound. Thus, while the associated type can be specified, it cannot be used in any way, because trait objects are not `Sized`.
= note: `#[warn(unused_associated_type_bounds)]` on by default
However, this warning isn't completely correct. This "type" can't be used, because it's not actually defined:[5]
<dyn AssocOptOut<Foo = ()> as AssocOptOut>::Foo
But the type in the equality bound can certainly be used!
impl<T: Default> AssocOptOut for Box<dyn AssocOptOut<Foo = T>> {
type Foo = T;
fn foo(&self) -> Self::Foo {
T::default()
}
}
When I initially found this, it seemed like an oversight: usually Rust language development takes a "let's be conservative" approach.[6] But now I realize that this had to be a warning in order to not be a breaking change: There was a window[7] where you could add where Self: Sized
to associated types but were still required to mention them in dyn Trait
.
So now I think the utility just wasn't anticipated. Well... potential utility I should say. I admit I haven't thought up any amazing examples of utilizing this ability. But then, I only stumbled across this today .
An any rate, I feel this demonstrates that specifying the associated type is conceivably useful. Fortunately, you can disable the warning. I think it should be reworded at a minimum.
Specifying GAT equality
The analogous syntax for GATs gives us "partial GAT equality" in dyn Trait
:
pub trait LifetimeGat {
type Gat<'a> where Self: Sized;
}
impl LifetimeGat for String {
type Gat<'a> = &'a str;
}
fn partial_specification<'a>() {
let s = String::new();
let _: &dyn LifetimeGat<Gat<'a> = &'a str> = &s;
let _: &dyn LifetimeGat<Gat<'static> = &'static str> = &s;
}
In fact, although this syntax is still not supported:
let _: &dyn LifetimeGat<for<'b> Gat<'b> = &'b str> = &s;
The new feature also gives us higher-ranked GAT equality in dyn Trait
, if we just move the binder:
fn hr_gat_equality() {
let s = String::new();
let _: &dyn for<'b> LifetimeGat<Gat<'b> = &'b str> = &s;
}
However, only if the GAT is non-dyn
-usable. As with the non-generic associated type example, I'm putting this in the "conceivably useful but I haven't thought up an awesome use case yet" pile.
That's all I've got for now
Hope it was at least interesting! I'm still mulling over the ability to specify non-dyn
-usable associated types and GATs. If you think up any neat use cases, please let me know below.
Here's a playground with most the sample code in case someone wants to play around with it.
They were in the release notes but not the announcement. Maybe I'm biased but I think it deserves more attention! ↩︎
also added to my tour, but I thought it was notable enough to make a post here too ↩︎
Maybe this limitation is why it wasn't considered a big deal? ↩︎
or well-formed, if you prefer ↩︎
which I agree with ↩︎
1.65..1.72
↩︎