--- title: "Meetup 2024: Expanding dyn-capability" tags: ["T-lang", "design-meeting", "minutes"] date: 2024-09-10 url: https://hackmd.io/l_EJFwlsT8KojFQIY-HoOQ --- # Expanding dyn-capability * What makes traits not dyn-capable? How can we fix it so that they are? * E.g. `dyn Iterator<Item=ConcreteType>` is (sometimes) unfortunate * If all I really want is `dyn Iterator<Item=dyn ItemBound>` (in effect; or maybe `dyn Iterator<dyn* ItemBound>`, not sure...), can we make that easier for people to reach for? * also, trait methods with generics will immediately be non dyn-capable. Can we expand things (namely for `fn method(&self, impl Bound)`) so that *that* is at least supported via some dyn magic. Specific problematic cases: Case 1. (returning) associated types from method that you might want to call from a dyn trait Case 2. generic method (aka a method with a generic type binding directly on it), particularly closures Case 1 has the "normal" workaround of specifying concrete type in `dyn Iterator<Item=ConcreteType>`. > ```rust > fn print(iterator: &mut impl Iterator<Item: Display>) {} > // means: > fn print<I: Iterator>(iterator: &mut I) > where > I::Item: Display > {} > ``` ```rust // no need to statically know `Item` fn print(iterator: &mut dyn Iterator<Item: Display>) { for item in iterator { println!("{item}"); } } ``` ```rust // no need to statically know `Item` fn print(iterator: dyn* Iterator<Item = dyn* Display>) { // dyn* Iterator<Item: Display> ==> dyn* Iterator<Item = dyn* Display> // impl Iterator<Item: Display> ==> impl Iterator<Item = impl Display> for item in iterator { println!("{item}"); } } ``` FWIW, [this compiles today](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=34291cc1808087b8b26b57891f61cb8a): ```rust fn print(iterator: impl Iterator<Item: Display>) {} ``` Niko's breakdown =) * "Erased trait" pattern from serde + friends * "Partially dyn" traits * "Partially known" associated types whose values you don't care about * Bounded associated types with dyn values Note: following rustc's suggestion to use `&mut dyn Iterator<Item = impl Display>` "works", except that it injects an implicit type parameter and causes you to generate multiple copies of the code during monomorphization of that type parameter. We could write `dyn Iterator<Item: dyn Display>`, which we would kinda want to mean "translate the item's concrete type to be a dyn". --- ```rust trait Foo { fn fmt(&self); } ``` * Could have "partial dyn" trait support, where you can turn (a reference to) any value implementing (any) Trait into an dyn Trait, and merely say that `dyn Trait` isn't allowed to invoke all of the non dyn-compatible methods. **But** consider the below, where one attempts to pass a `&mut dyn Trait` to a function taking an `&mut impl Trait`: ```rust fn foo(f: &mut dyn Trait) { bar(f); // still has the idea of "dyn compatibility" -- does `dyn Trait: Trait` } fn bar<F: ?Sized + Trait>(f: &mut F) { } ``` Supporting the above is then problematic, because `bar` has no limits on what methods of Trait it might invoke. ---- (Side note (?):) there is a gotcha regarding how dispatch ends up resolving method calls: you may end up in the default method (analogous to a base class method in C++) when you might have expected dispatch to occur; see below: ```rust trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; fn map<F: FnMut>(self, f: F) -> Map<Self, F> where Self: Sized; } impl<I> Iterator for &mut I where I: ?Sized + Iterator, {} ``` * `<I as Iterator>::map` -- ok, "optimized" * `<dyn Iterator as Iterator>::map` -- inaccessible * `<&mut dyn Iterator<Item = X> as Iterator>::map` -- the default but going through vtable for each `next` call An example of the current workaround without `final`: ```rust pub trait AsyncGen { type Item; fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>>; } pub trait AsyncGenNext: AsyncGen { type Next<'s>: Future<Output = Option<Self::Item>> where Self: 's; fn next(self: Pin<&mut Self>) -> Self::Next<'_>; } impl<T: AsyncGen + ?Sized> AsyncGenNext for T { type Next<'s> = impl Future<Output = Option<Self::Item>> where Self: 's; fn next(mut self: Pin<&mut Self>) -> Self::Next<'_> { core::future::poll_fn(move |cx| self.as_mut().poll_next(cx)) } } ``` Here's the impl for `&mut impl Iterator`, which is what `&mut dyn Iterator` hits: <https://doc.rust-lang.org/1.81.0/src/core/iter/traits/iterator.rs.html#4106> --- How can we just make it possible to express OO patterns in Rust with dyn? ```rust dyn trait Foo {} ``` Niko: Wanting to subset comes up fairly often in practice. Tyler: Works; sometimes you want to let the caller choose if they want dyn-dispatch which wouldn't work for traits that have non-dyn-compatible method Scott: You could subset with something like `impl dyn Foo` --- What makes dyn so hard to use? The fact that it's not sized is a big one. (and that was motivation for [`dyn*`...](https://smallcultfollowing.com/babysteps/blog/2022/03/29/dyn-can-we-make-dyn-sized/)) --- Regarding case 2 above, we can special case `impl Trait` (as opposed to arbirary generic methods) like so: ```rust trait GiveFuture { fn method(&self, f: impl Future<Output = u32>); } // can be emulated roughly like so impl GiveFuture for dyn GiveFuture { fn method(&self, mut f: impl Future<Output = u32>) { let f: &mut dyn Future<Output = u32> = &mut f; <$UnderlyingType as GiveFuture>::method::<&mut dyn Future<Output = u32>>(self, f) // vtable dispatch } } // relies on // * impl Future for &mut dyn Future // * `impl Future: Sized` // * if `Future: 'static` bound, this wouldn't work // * does this matter too much?? ``` ```rust trait RetFuture { async fn method(&self) -> u32; } trait RetFuture { fn method(&self) -> impl Future<Output = u32>: } ``` ```rust! trait HasOpaqueAT { type AT; fn gives(&self) -> AT; fn takes(&self, at: AT); } // In theory this could be `dyn*`-capable if you transform it such that `gives` returns an opaque thing and `takes` takes that same opaque thing. ``` --- Summary: * Could redefine "dyn compatibility" to mean "cannot implement the trait" * means `&dyn Trait` only calls subset of dyn-compatible methods * would allow us to e.g. have "missing associated types" but they rule out parts of the trait * however means that `dyn Trait` does not (always) implement `Trait` * similar-but-different to `where Self: Sized` * might give an extra degree of freedom to "fix dyn-compatible rules" * seems like we have some alignment here! * Could have a name for "the subset of the trait that is dyn compatible" * but it is not a "single" subset (`dyn Iterator` / `dyn Iterator<Item = u32>`) * Or more generally, a trait transformer that can subset or transform method signatures to make them dyn compatible. * Name placeholder: `impl dyn Trait` * Generic method that take APIT (or an APIT-like pattern) can sometimes be made virtual * you instantiate the generic method with `APIT=&mut dyn Trait` and put that in the vtable * this only works if `&mut dyn Trait: Trait` (see above) but it gets broken by `'static` bounds and some other corner cases * Something like `dyn*` would also expand because `fn(self)` would work and many other limitations arise from `dyn Foo` not being `Sized` * Could add an `impl dyn Trait` (NEED A BETTER NAME) that is an `impl Trait` that only lets you call the dyn-capable subset, and you can then pass any `dyn Trait` to it.