Simulating a call to "parent" virtual method in Rust

Lise Henry

Crowbook is a tool that renders Markdown books (or articles) to HTML, Epub and PDF. In summary it works like this.

First, a parser (using pulldown-cmark) reads all the Markdown files and outputs some kind of AST (Abstract Syntax Tree), which is basically a list of Tokens, which look like this:

enum Token {
    Str(String),
    Paragraph(Vec<Token>),
    Header(i32, Vec<Token>),
    Emphasis(Vec<Token>),
    ...
}

Then, a Book groups all the ASTs corresponding to each Markdown file, information about the book options (author, title, where to ouput the files, and so on).

Finally, the book is rendered in various formats, using different renderers. There are currently 5 different renderers:

  • LatexRenderer renders to text;
  • OdtRenderer renders to Odt (well, it doesn't really, because it doesn't really work currently, but the renderer exists anyway);
  • HtmlRenderer renders to a single, self-contained HTML file;
  • HtmlDirRenderer renders to multiple HTML files;
  • EpubRenderer renders to an Epub file.

Now, let's look at the last three. Obviously, they share much of their job, because they all deal with generating HTML, and, for most variants of Token, they generate exactly the same code (the only necessary differences are for Links, since internal links must be a bit tweaked to work when there are multiple files. and for Images, which are included as base64 code in the single self-contained file).

Now, had I written Crowbook in an object-oriented programming language, I would have had an obvious solution in mind: define a Renderer interface or abstact class, then have all renderers implement it/extend it. Then I guess I could have all three HTML-based renderers derive from a common class. So, basically, something like this ugly pseudo-UML graph:

Class hierarchy

Now, Rust isn't object-oriented, so you can't really do that. There are, however, traits, that are more or less similar-ish to interfaces in Java. So, all Renderers can implement a Renderer trait. And, what's cool is that traits can have default implementations, so we can avoid duplicating code:

trait Renderer {
    fn render_token(&mut self, token: &Token) -> Result<String>;
    fn render_vec(&mut self, tokens: &[Token]) -> Result<String> {
        tokens.iter()
            .map(|token| self.render_token(token))
            .collect::<Result<Vec<_>>>()
            .map(|vec| vec.join(""))
    }
}

So, we have a render_token method that must be implemented by each renderer, and render_vec which is basically a fancy for loop.

And, well, for the renderers that are really different, that's quite enough, because there isn't that much similarity between generating LaTeX and HTML.

However, there are three differents HTML renderers, who share a bit more code.

Now, there is this whole idea of composition over inheritance which, as far as I get it, is that you can avoid inheritance by composing elements. So, in this light, it would seem logical that each HTML-based renderer include the HTML common renderer[1] and call it when necessary.

Now, this looks nice, because if the EPUB renderer just has to render Emphasis tokens with <i> instead of <em>[2], you could do something like that:

impl Renderer for EpubRenderer {
    fn render_token(&mut self, token: &Token) -> String {
        match *token {
            Token::Emphasis(ref inner) => format!("<i>{}</i>", self.render_vec(inner)),
            _ => self.html.render_token(token)
        }
    }
}

That is, render Emphasis variant of Token differently, but fall back on the HTML implementation to render all other variants.

In practice, unfortunately, this doesn't work, as you can see with the full example on Rust playground. The problem is that unlike a call to super in an object-oriented language, calling self.html.render_token means that every other methods call will be from self.html. So, the interest of having a polymorphic method (thanks to trait) is lost.

Now, is there a way to emulate this kind of "inheritance" behaviour with trait? Clearly, using only the Renderer trait is not enough. The good news, though, is that it's still possible to have some kind of polymorphism similar to virtual methods, even when there was a previous call to a default implementation.

The idea is to move the implementation of Renderer for HtmlRenderer in a separate, standalone function, and to make the impl block call it:

struct HtmlRenderer {
    n_para: u32,
}
    
fn html_render_token<T:AsMut<HtmlRenderer>+Renderer>(this: &mut T, token: &Token) -> String {
    match *token {
        Token::Str(ref content) => content.clone(),
        Token::Paragraph(ref inner) => {
            this.as_mut().n_para += 1;
            let n = this.as_mut().n_para;
            format!("<p id = '{}'>{}</p>", n, this.render_vec(inner))
        },
        Token::Emphasis(ref inner) => format!("<em>{}</em>", this.render_vec(inner))
    }
}

impl AsMut<HtmlRenderer> for HtmlRenderer {
    fn as_mut(&mut self) -> &mut HtmlRenderer {
        self
    }
}

impl Renderer for HtmlRenderer {
    fn render_token(&mut self, token: &Token) -> String {
        html_render_token(self, token)
    }
}

Now, this separate function is basically the same code as the previous implementation of Renderer, except it is now for AsMut<HtmlRenderer>. This necessitates some boilerplate (implementing AsMut for HtmlRenderer), but the good news is that now, we can have EpubRenderer call it without problem:

impl AsMut<HtmlRenderer> for EpubRenderer {
    fn as_mut(&mut self) -> &mut HtmlRenderer {
        &mut self.html
    }
}

impl Renderer for EpubRenderer {
    fn render_token(&mut self, token: &Token) -> String {
        match *token {
            Token::Emphasis(ref inner) => format!("<i>{}</i>", self.render_vec(inner)),
            _ => html_render_token(self, token),
        }
    }
}

Notice that when html_render_token is called, it is with self and not self.html. This means that, inside of it, it is the correct implementation that will be called. Therefore,

let ast = // ...
let mut html = HtmlRenderer::new();
let mut epub = EpubRenderer::new();
println!("{}", html.render_vec(&ast));
println!("{}", epub.render_vec(&ast));

gives us different results, with the HTML printing <em> and the EPUB one correctly printing <i>.

So, is it possible to do inheritance in Rust? No. Is it possible to simulate a call to a parent virtual method by (ab)using traits and with some perseverance? Yes. It took me a while to come to this solution, but I feel that the boilerplate it adds, even if it is more complicated than a call to super.foo() isn't actually that bad[3].

Thanks for all the people on Rust's reddit, I definitely wouldn't have come with this idea without your help :)

Full working example is on Rust Playground.

Notes

[1]

[2]

[3]