---
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.