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.