Why can't a dyn compatible trait use generics

I am working with dynamic dispaching in rust and I understand how it works (creating a vtable ect) the one thing I don't understand is why the functions inside these traits can't have function parameters.

My trait looks like this:

pub trait Number: Float + fmt::Debug + Any {}
impl<T: Float + fmt::Debug + Any> Number for T {}

pub trait Math<N: Number> {
    fn dbg(&self, f: &mut fmt::Formatter<'_> -> fmt::Result;
    fn cloned(&self) -> Box<dyn Math<N>>;
    fn evaluate(&self, variables: &HashMap<&str, Box<dyn Math<N>>>) -> EvalResult<N>;

    // function I would like to add but can't
    fn map<F: Fn(N) -> O, O: Number>(&self, fn: F) -> Box<dyn Math<N>>;
}

What I don't understand is why the rules of dynamic traits can use functions with generic types.

With typical generic functions rust creates an implementation for each function, i.e.

fn main() {
    foo(32);
    foo(12.0);
    foo(false);
}

fn foo<T: fmt::Debug>(val: T) {
    println!("{val:?}")
}

generates a function for each used case

fn main() {
    foo_i32(32);
    foo_f64(12.0);
    foo_bool(false);
}

fn foo_i32(val: i32) {
    println!("{val:?}")
}

fn foo_f64(val: f64) {
    println!("{val:?}")
}

fn foo_bool(val: bool) {
    println!("{val:?}")
}

so why can a dyn compatible trait do the same

fn main() {
    let a = FooA::<f64>::new(
        FooB::<f64>::new(
            FooA::<f64>::new(12.0), 
            FooA::f64::new(-1.0)
        )
    );
    let b = a.cloned().map::<f32, _>(|x| x as i32);
    let c = a.cloned().map::<bool, _>(|x| x > 0.0);
    let d = b.cloned().map::<f32, _>(|x| x * x);

    println!("a: {a:?}"); // FooA { val: FooB { a: FooA { val: 12.0 }, b: FooA { val: -1.0 } }  }
    println!("b: {b:?}"); // FooA { val: FooB { a: FooA { val: 12.0 }, b: FooA { val: -1.0 } }  }
    println!("c: {c:?}"); // FooA { val: FooB { a: FooA { val: true }, b: FooA { val: false } }  }
    println!("d: {d:?}"); // FooA { val: FooB { a: FooA { val: 144.0 }, b: FooA { val: 1.0 } }  }
}

trait Foo<T> {
    fn map<F: Fn(T) ->O, O>(&self, func: F)) -> Box<dyn Foo<O>>;
    fn cloned(&self) -> Box<dyn Foo<T>> where T: Clone;
    fn dbg(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result where T: fmt::Debug;
}

#[derive(Debug, Clone)]
struct FooA<T> {
    val: Box<dyn Foo<T>>
}

impl<T> FooA<T> {
    fn new(val: impl Foo<T>) -> Self {
        FooA { val: Box::new(val) }
    }
}

impl<T> Foo<T> for FooA<T> {
    fn map<O: From<T>>(&self) -> Box<dyn Foo<O>> {
        Box::new(FooA::<O>::new(self.val.into()))
    }

    fn cloned(&self) -> Box<dyn Foo<T>> 
    where
        T: Clone
    {
        Box::new(self.clone())
    }

    fn dbg(&self, f: &mut Formatter<'_>) -> fmt::Result
    where
        T: fmt::Debug
    {
        <Self as fmt::Debug>::fmt(self, f)
    }
}

#[derive(Debug)]
struct FooB<T> {
    a: Box<dyn Foo<T>>,
    b: Box<dyn Foo<T>>,
}

impl<T: Clone> Clone for FooB<T> {
    fn clone(&self) -> Self {
        FooB {
            a: self.a.cloned(),
            b: self.b.cloned()
        }
    }
}

impl<T> FooB<T> {
    fn new(a: impl Foo<T>, b: impl Foo<T>) -> Self {
        FooB {
            a: Box::new(a),
            b: Box::new(b)
        }
    }
}

impl<T> Foo<T> for FooB<T> {
    fn map<F: Fn(&T) -> O, O>(&self, func: F) -> Box<dyn Foo<O>> {
        Box::new( FooB {
            a: self.a.map(func),
            b: self.b.map(func)
        })
    }

    fn cloned(&self) -> Box<dyn Foo<T>>
    where
        T: Clone
    {
        Box::new(self.clone())
    }

    fn dbg(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
    where
        T: fmt::Debug
    {
        <Self as fmt::Debug>::fmt(self, f)
    }
}

impl<T> Foo<T> for T {
    fn map::<F: Fn(&T) -> O, O>(&self, func: F) -> Box<dyn Foo<O>> {
        Box::new(func(self))
    }

    fn cloned(&self) -> Box<dyn Foo<T>> 
    where
        T: Clone
    {
        Box::new(self.clone())
    }

    fn dbg(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        <Self as fmt::Debug>::fmt(self, f)
    }
}

impl<T: fmt::Debug> fmt::Debug for dyn Foo<T> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.dbg(f)
    }
}

which would generate the following vtables:

// Foo_f64
fn foo_f64_map_f32_{closure_id}(&self, func: {closure_id}) -> Box<dyn Foo_f32> { .. }
fn foo_f64_map_bool_{closure_id}(&self, func: {closure_id}) -> Box<dyn Foo_bool> { .. }
fn foo_f64_cloned(&self) -> Box<dyn Foo_f64> { .. }
fn foo_f64_dbg(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { .. }

// Foo_f32
fn foo_f32_map_f32_{closure_id}(&self, func: {closure_id}) -> Box<dyn Foo_f32> { .. }
fn foo_f32_cloned(&self) -> Box<dyn Foo_f32> { .. }
fn foo_f32_dbg(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { .. }

// Foo_bool
fn foo_bool_dbg(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { .. }

which would generate this vtable for both FooA and FooB

it seems that in any case only a finite number of functions need to be created in the vtable for each instantiated implementer of Foo

The vtable has to be the same for all implementers.

Also, the number is unbounded. Like, let's say you thought the vtable only needed 122 method entries...

// Some downstream crate
fn example(f: &dyn Foo<f64>) {
    // Every closure is a unique type
    foo.map(|_| {});
    foo.map(|_| println!("hi"));
    foo.map(|_| 0);
    foo.map(|_| MyLocalType::default());
}

Oops, it needs 126 now.

It could perhaps be done with global analysis, but that would be quite expensive, require at least some amount of recompilation everytime something downstream changed, and result in massive vtables.

It could perhaps be allowed with some sort of sealed trait bound so that all possible invocations were known within a single crate, but we don't have something like that yet. It would be a breaking change to "remove the seal" if we did. It would still require crate-wide analysis.

3 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.