Alternative to impl trait in fn pointer return type

Let's pretend the following syntax is valid:

struct Command {
    doc: fn() -> impl std::fmt::Display,
}

Library code would internally call the user-supplied Command.doc() function, and would use its output to build a string and print.

Is there an alternative solution to that syntax that achieves the same goal? That might be too broad, here's some context..

Context
I'm building a library for writing structured text, e.g. CLI Help text. There's a default doc_gen function that builds the help text for every command. Then, each command can build onto it with additional help text.

My idea was to have these doc_gen functions return a set of nestable formatting elements so they can compose, and then the final renderer would take these elements as input and render them into a string. The issue is, I assume my implementation of the formatting elements will be wrong, and want users to be able to come up with better ways of generating their text while my library just expects it to be able to be turned into a string.

Maybe the internal render function could call a user-defined render function, given in some initialization step, so the library wouldn't necessarily need to care about generating the string or printing. But, then I don't know how I'd supply the default rendering function as part of the library..

Does anyone have experience with this sort of problem that could offer some suggestions or alternative patterns?

A core goal of the library is minimum memory usage.

EDIT: A more conceptually complete explanation of the use case below..

The code below is but a conceptual shell to try to explain the core flow of the program with all unnecessary bits removed. If interested, the full code for this project is at https://github.com/slanden/rust-router. A relevant example is in examples/with_docs.rs.

/// A command, as in a CLI command
struct Command {
    name: &'static str,
    /// A function that modifies the default documentation in some
    /// way, such as adding command-specific documentation
    doc: fn(c: &Context, doc: DocNodeWithoutSummary) -> DocNode,
}

/// Documentation is built up into a hierarchy where some
/// nodes are meant for text and some are for
/// structure/formatting, like markup
struct DocNode {
    text: &'static str,
    children: &'static [DocNode],
}

/// When users define their commands, it's ok to not specify
/// a `doc()` method, but when they do, their documentation
/// should include a summary. The logic for enforcing this
/// is not shown here, but they would need to call a method
/// on this struct that adds the summary for their command,
/// then they get `DocNode`s and are free to add whatever
/// else.
pub struct DocNodeWithoutSummary {
    blocks: DocNode,
}

/// Like CLI argument parsers, the command an end-user wants
/// is selected. With the selected command, other arguments
/// that were passed with it are stored in a `Context` along with
/// some other information they can use in their docs, such as
/// the name and parameters of the selected command.
struct Context {}
/// Convert the hierarchy to its final string form
impl std::fmt::Display for DocNode {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        // ...
        Ok(())
    }
}

/// Find which command and arguments the user passed in
fn find_selected_command(
    args: std::env::Args,
    commands: &Vec<Command>,
) -> usize {
    // Just an example
    0
}

fn default_docs(
    c: &Context,
    commands: &Vec<Command>,
) -> DocNodeWithoutSummary {
    DocNodeWithoutSummary {
        blocks: DocNode {
            text: "Here is some default documentation for every command",
            children: Vec::new(),
        },
    }
}

fn render_a_commands_documentation(commands: Vec<Command>) {
    let selected_cmd_index =
        find_selected_command(std::env::args(), &commands);
    let context = Context {};
    let doc = default_docs(&context, &commands);
    let final_doc = (commands[selected_cmd_index].doc)(&context, doc);
    println!("{}", final_doc);
}

fn man() {
    // The user defines their program/API
    let commands = vec![Command {
        name: "example",
        doc: |c: &Context, existing_doc_nodes| {
            let mut modified_docs = existing_doc_nodes.blocks;
            modified_docs.children.push(DocNode {
                text: "Some documentation, examples, etc. for the program named 'example'",
                children: vec![]
            });
            modified_docs
        },
    }];

    // This would parse any passed-in arguments, and call
    // the real program code defined for the selected
    // command, but for this example that logic is removed
    // and emphasis is on documentation
    render_a_commands_documentation(commands);
}

Naively, the most direct adaptation would be to switch to returning Box<dyn Display>.


But you mention “memory usage”, by which you might mean heap memory in particular, so additional boxing might be undesired.

To really explore alternatives, if they exist, I’d need to better understand the use-case though.

With no further understanding on whether or not this could possibly be of any use, here’s a thought that came to mind though: For example, one can work with trait objects on the stack just fine, if you read them by-reference. To that effect, a signature accepting a callback could avoid the need for Box at the cost of only giving a short-lifetime view during the call-back… like

doc: fn(&mut dyn FnOnce(&dyn std::fmt::Display))
2 Likes

Isn't the usual way of solving this is to turn the problem inside out, and instead of requesting a Display type to be returned, you instead pass in an fmt::Formatter<'_> that the type can write itself into?

6 Likes

Thanks for the suggestion and trying to understand what I'm asking for. I tried your suggestion, but I got an error and didn't quite understand it or the code itself so I tried to add a better explanation of the use case to my original post. I also linked to the repository if you wanted to dig deeper, but I was trying to save people from that lol.

I'm trying to allow a function in user code to modify what library code would generate. So, I need to work with something like a set of nodes that can be added to, removed, or have others inserted in the middle. After these modifications take place, only then would the library code be able to write to something like a string. The "set of nodes", i.e. the type and implementation of the documentation generation machinery, is what I'm trying to allow the user of the library to define.

I'm not entirely sure what this has to do with the original problem, though. Requiring an impl Display to be returned is equivalent with passing in a Formatter and requiring it to be written to.

(I thought it was) not equivalent since writing to a formatter restricts the order in which the strings appear.

If I ask for 3 different impl Displays I can write them in any order. If I hand out a formatter 3 times, I get strings written in the order I handed them out.

Having said that, one could hand out separate formatters and then combine the strings later, in which case it is equivalent!

@paramagnetic I think I see what you're saying. What I would probably need, if the original syntax worked, was to have a second trait where the return might be like -> impl Display + DocNodeOperations where the DocNodeOperations would be the possible operations a DocNode could do. @drmason13 brings up a good point also.

The conversion to string is the last step, called in library code. But, there can be multiple Commands, each with its own doc() function and each could potentially modify some other Commands DocNodes. Modifications could happen in any order.

So, a string is not built up for each Command, but the string conversion happens once at the end.