Why can't a dyn compatible trait use generics

⚓ Rust    📅 2025-09-22    👤 surdeus    👁️ 7      

surdeus

Warning

This post was published 39 days ago. The information described in this article may have changed.

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

2 posts - 2 participants

Read full topic

🏷️ Rust_feed