Calling between dynamic and static dispatch

ugh, back again. This is a continuation of Is it possible to call a static dispatch function with a trait object? . Adding the ?Sized unfortunately broke calls going the other way (from static dispatch to dynamic dispatch) but, just as before, going through a wrapper in the trait itself makes it work so it should be possible. Anyone know how I can have dynamic_vocalize and static_vocalize call each other without having to jump through a wrapper in the trait?

(please note that you will get an infinite recursion leading to a stack overflow if you run the code. This is just a minimal example to cause the problem, not my actual end code :slight_smile: )

trait Animal {
    fn wrapped_dynamic_vocalize(&self);
    fn wrapped_static_vocalize(&self);
}

struct Cat;

impl Animal for Cat {
    fn wrapped_dynamic_vocalize(&self) {
        dynamic_vocalize(self);
    }
    fn wrapped_static_vocalize(&self) {
        static_vocalize(self);
    }
}

fn dynamic_vocalize(animal: &dyn Animal) {
    println!("Dynamic vocalize");
    // Works fine:
    animal.wrapped_static_vocalize();
    // Does not work unless I keep "+ ?Sized" to static_vocalize:
    // static_vocalize(animal);
}
fn static_vocalize<A: Animal + ?Sized>(animal: &A) {
    println!("static_vocalize");
    // Works fine:
    animal.wrapped_dynamic_vocalize();
    // Does not work unless I delete "+ ?Sized":
    // dynamic_vocalize(animal);
}

fn main() {
    let c = Cat;
    static_vocalize(&c);
    dynamic_vocalize(&c);
}

Well, you have a set of mutually recursive functions. You need to provide a concrete implementation that doesn't just defer to something else somewhere.

You could add the following impl for references, that just defers to the pointee

impl<A: ?Sized + Animal> Animal for &A {
    fn wrapped_dynamic_vocalize(&self) {
        A::wrapped_dynamic_vocalize(self)
    }
    
    fn wrapped_static_vocalize(&self) {
        A::wrapped_static_vocalize(self)
    }
}

Then you can do

fn static_vocalize<A: Animal + ?Sized>(animal: &A) {
    println!("static_vocalize");
    // Works fine:
    animal.wrapped_dynamic_vocalize();
    // Does not work unless I delete "+ ?Sized":
    dynamic_vocalize(&animal);
}

But this is still a set of mutually recursive functions, not that different from

fn foo() { bar() }
fn bar() { foo() }
1 Like

I'm not sure I fully understood your post, but I didn't know about being able to use generics for trait implementations. Using that, I was able to set up a Wrapped trait that let me wrap all the functions once for all Animals instead of having to wrap it for each and every Animal which is a HUGE improvement! Unfortunately it still relies on going through the wrappers in the traits. Ideally I'd like to be able to get rid of all wrapped_ calls entirely since they exist only to prove that calling between the two functions is possible. So to put in in more concrete terms, using what I learned from your post I now have:

trait Wrapped {
    fn wrapped_dynamic_vocalize(&self);
    fn wrapped_static_vocalize(&self);
}

trait Animal: Wrapped {
    fn vocalize(&self);
}

struct Cat;
struct Dog;

impl<A: Animal> Wrapped for A {
    fn wrapped_dynamic_vocalize(&self) {
        dynamic_vocalize(self);
    }

    fn wrapped_static_vocalize(&self) {
        static_vocalize(self);
    }
}

impl Animal for Cat {
    fn vocalize(&self) {
        println!("meow");
    }
}

impl Animal for Dog {
    fn vocalize(&self) {
        println!("woof");
    }
}

fn dynamic_vocalize(animal: &dyn Animal) {
    println!("Dynamic vocalize");
    // Works fine:
    animal.wrapped_static_vocalize();
    // Still does not work unless I keep "+ ?Sized" to static_vocalize:
    // static_vocalize(animal);
}
fn static_vocalize<A: Animal + ?Sized>(animal: &A) {
    println!("static_vocalize");
    // Works fine:
    animal.wrapped_dynamic_vocalize();
    // Still does not work unless I delete "+ ?Sized":
    // dynamic_vocalize(animal);
}

fn main() {
    let c = Cat;
    let d = Dog;
    static_vocalize(&c);
    dynamic_vocalize(&d);
}

but what I'm trying to get to is:

trait Animal {}

struct Cat;

impl Animal for Cat {}

fn dynamic_vocalize(animal: &dyn Animal) {
    println!("Dynamic vocalize");
    // Still does not work unless I keep "+ ?Sized" to static_vocalize:
    static_vocalize(animal);
}
fn static_vocalize<A: Animal + ?Sized>(animal: &A) {
    println!("static_vocalize");
    // Still does not work unless I delete "+ ?Sized":
    dynamic_vocalize(animal);
}

fn main() {
    let c = Cat;
    static_vocalize(&c);
}

which should be possible as proven through the trait wrappers, but may not yet be possible in rust? Like the trait wrappers proved it can be accomplished in safe, simple, but unfortunately verbose code.

There is a fundemental problem with your code. You are trying to create two functions that call each other unconditionally. This is why you are getting infinite recursion and blowing the call stack.

Let's trace the program you are trying to create in your second example.

This one

Trace

main:
    static_vocalize<Cat>:
        println
        dynamic_vocalize:
            println
            static_vocalize<dyn Animal>:
                println
                dynamic_vocalize:
                    static_vocalize<dyn Animal>: # oh no, we've seen this before
                        println
                        dynamic_vocalize ...

So you are guaranteed to blow the call stack

The way to fix this is to make either dynamic_vocalize or static_vocalize call something else. For example, Animal::vocalize

playground

trait Animal {
    fn vocalize(&self);
}

struct Cat;

impl Animal for Cat {
    fn vocalize(&self) {
        println!("meow");
    }
}

fn dynamic_vocalize(animal: &dyn Animal) {
    println!("Dynamic vocalize");
    static_vocalize(animal);
}
fn static_vocalize<A: Animal + ?Sized>(animal: &A) {
    println!("static_vocalize");
    animal.vocalize();
}

fn main() {
    let c = Cat;
    static_vocalize(&c);
    dynamic_vocalize(&c);
}

oh! no we have a misunderstanding. The infinite recursion isn't the issue. The issue is the code won't compile. I'm fully aware of the infinite recursion, it was just the simplest way to set up a minimal example of calling between a trait object function (&dyn Animal) and a static dispatch function (<A: Animal>). You can see the compile error here.

In case you're curious, my real code that is calling between these two types of functions is I'm trying to implement a rust version of the DustJS template engine. So for each value in the render context I have a trait they're implementing called ContextElement

pub trait ContextElement: Debug + Walkable + Renderable + Loopable {}

Which supports walking down a context, for example:

context = {"animal": {"cat": {"food": "meat", "enemies": "water"}}}
context.walk("animal").walk("cat") == {"food": "meat", "enemies": "water"}

For example, an implementation of Walkable for HashMap would be:

        impl<I: ContextElement> Walkable for HashMap<&str, I> {
            fn walk(&self, segment: &str) -> Result<&dyn ContextElement, RenderError> {
                let child = self.get(segment).ok_or(RenderError::WontWalk {
                    segment: segment.to_string(),
                    elem: self,
                })?;
                Ok(child)
            }
        }

but paths can have multiple segments. For example in a dust template I could write:

{animal.cat.food}

and it would print meat.

So I have a recursive function to walk down the elements of the path. Since the type for the object at each step of the way can't be known, it must use trait objects (I assume, I have a minimal example of these traits using generics and I haven't been able to get a working version for the walk_path function.):

fn walk_path<'a>(
    context: &'a dyn ContextElement,
    path: &Vec<&str>,
) -> Result<&'a dyn ContextElement, RenderError<'a>> {
    let mut output = context;

    for elem in path.iter() {
        output = output.walk(elem)?;
    }

    Ok(output)
}

So the problem happens when I have whats called a "section" in dust which is a part of the template thats looped over with a new context. For example:

context = ["foo", "bar", "baz"]
{#context}{.}{/context}

This would print "foobarbaz" but it accomplishes it by rendering the section body ({.}) with the context "foo", then "bar", then "baz".

So for example, the calls for rendering the above would be something like:

render_body("{#context}{.}{/context}", {"context": ["foo", "bar", "baz"]})
new_context = walk_path("context")
for element in new_context {
    render_body("{.}", element)
}

However, I wanted render_body to be a static dispatch function using generics rather than a dynamic dispatch function taking in &dyn trait objects:

impl<'a> DustRenderer<'a> {
    pub fn render<C>(&self, name: &str, context: &'a C) -> Result<String, RenderError<'a>>
    where
        C: ContextElement,
    {
        let main_template = match self.templates.get(name) {
            Some(tmpl) => tmpl,
            None => {
                return Err(RenderError::Generic(format!(
                    "No template named {} in context",
                    name
                )));
            }
        };
        self.render_body(&main_template.contents, context)
    }

    fn render_body<C>(&self, body: &Body, context: &'a C) -> Result<String, RenderError<'a>>
    where
        C: ContextElement,
    {
        let mut output = String::new();
        for elem in &body.elements {
            match elem {
                TemplateElement::TESpan(span) => output.push_str(span.contents),
                TemplateElement::TETag(dt) => {
                    output.push_str(&self.render_tag(dt, context)?);
                }
            }
        }
        Ok(output)
    }

    fn render_tag<C>(&self, tag: &DustTag, context: &'a C) -> Result<String, RenderError<'a>>
    where
        C: ContextElement,
    {
        match tag {
            DustTag::DTComment(_comment) => (),
            DustTag::DTReference(reference) => {
                let val = walk_path(context, &reference.path.keys);
                if let Err(RenderError::WontWalk { .. }) = val {
                    // If reference does not exist in the context, it becomes an empty string
                    return Ok("".to_owned());
                } else {
                    return val?.render(&reference.filters);
                }
            }
            DustTag::DTSection(container) => {
                let val = walk_path(context, &container.path.keys);
                if let Err(RenderError::WontWalk { .. }) = val {
                    // If reference does not exist in the context, it becomes an empty string
                    return Ok("".to_owned());
                } else {
                    let loop_elements: Vec<&dyn ContextElement> = val?.get_loop_elements()?;
                    if loop_elements.is_empty() {
                        // Oddly enough if the value is falsey (like
                        // an empty array or null), Dust uses the
                        // original context before walking the path as
                        // the context for rendering the else block
                        //
                        // TODO: do filters apply? I don't think so
                        // but I should test
                        return match &container.else_contents {
                            Some(body) => self.render_body(&body, context),
                            None => Ok("".to_owned()),
                        };
                    } else {
                        match container.contents {
                            None => Ok("".to_owned()),
                            Some(body) => {
                                let rendered_results: Result<Vec<String>, RenderError> =
                                    loop_elements
                                        .into_iter()
                                        .map(|array_elem| self.render_body(&body, array_elem))
                                        .collect();
                                let rendered_slice: &[String] = &rendered_results?;
                                return Ok(rendered_slice.join(""));
                            }
                        };
                        return Ok("".to_owned());
                    }
                }
            }
            _ => (), // TODO: Implement the rest
        }
        Ok("".to_owned())
    }
}

So I have functions taking in a &dyn ContextElement and functions taking a <C: ContextElement> and I'm trying to figure out how to get them to be able to call each other. My drastically simplified code involving infinite recursion is just a minimal example reproducing the compilation issue so people don't have to dig through the complexities of my renderer code just to get down to the core issue.

1 Like

You could have impl<C: ConextElement + ?Sized> ContectElement for &C. Then you can just pas &&dyn ContextElement to your static dispatch function.

AnimalContext::wrapped_static_vocalize is doing something fundamental here, and you need it if you are going from a dynamic context back to a static one.

Specifically, there is one wrapped_static_vocalize function generated per trait implementat, and that function is stored in the trait's vtable. Without function pointer in the trait's vtable, there's no way to go from "some dyn implementation" into "a specific concrete implementation of the static_vocalize function with the correct type".

Adding the function to the trait is in essence telling rust to create one monomorphized instance of static_vocalize per trait implementation. That then provides a guarantee that every Animal passed in using &dyn Animal will have such an implementation, and will provide a way for the code to link to it (via the vtable).

One workaround for this would be to split the trait up into multiple ones, so that the wrapper methods could be auto-implemented. It isn't particularly pretty, though:

trait AnimalMethods {
    // regular functionality
}
trait SizedAnimal {
    fn wrapped_static_vocalize(&self);
}
trait Animal: AnimalMethods + SizedAnimal {}

impl<T: AnimalMethods + Sized> SizedAnimal for T {
    fn wrapped_static_vocalize(&self) {
        static_vocalize(self);
    }
}
impl<T: SizedAnimal + AnimalMethods> Animal for T {}

(playground)


Alternatively, if you don't care about actually having the underlying static type, you could write an implementation

impl<T> Animal for &'_ T where T: Animal { ... }

Then dynamic_vocalize could just use static_vocalize(&animal), and any method calls inside static_vocalize would essentially go through that static implementation to use the underlying dynamic dispatch. The downside is that next time you call into dynamic_vocalize from within that static_vocalize call, you'd end up using double dynamic dispatch, and then next round trip triple dynamic dispatch, etc. For a few layers in non-performance-sensitive code, it's fine, but the deeper you go the more costly each trait method call would get.

Ah thats what I was worried about. I guess the trait wrappers are necessary. Thanks for explaining that and confirming my (minor) fears. Also Thanks @RustyYato for bearing with me in this thread and showing me that I could use generics for impl blocks for traits, that SIGNIFICANTLY cleaned up the code by allowing a single wrapper implementation for all ContextElements.

Just for completeness or posterity or whatever, accepting that I need to trait wrapper as @daboross confirmed, I was able to implement the trait wrapper as:

pub trait RenderWrapper {
    fn render_body<'a>(
        &'a self,
        renderer: &'a DustRenderer,
        body: &Body,
    ) -> Result<String, RenderError>;
}

impl<C: ContextElement> RenderWrapper for C {
    fn render_body<'a>(
        &'a self,
        renderer: &'a DustRenderer,
        body: &Body,
    ) -> Result<String, RenderError<'a>> {
        renderer.render_body(body, self)
    }
}

Then I added RenderWrapper to the list of traits composing a ContextElement:

pub trait ContextElement: Debug + RenderWrapper + Walkable + Renderable + Loopable {}

and then I just had to change the call of:

.map(|array_elem| self.render_body(&body, array_elem))

to

.map(|array_elem| array_elem.render_body(self, &body))

Thank you both! I've been hung up on this all week and its a relief to finally be able to move forward.

2 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.