pacak/bpaf

no supported way to compose a set of alternative parsers from a slice or iterator

Closed this issue · 7 comments

I'm working on a tool that has a bunch of very similar subcommands, and so I would like to be able to do something like this:

use std::path::PathBuf;
use bpaf::*;
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
enum TreeWalkMode {
    Verify,
    Update,
    UpdateModified,
    Recalculate,
    Remove,
}
#[derive(Debug, Clone)]
pub(crate) struct Args {
    mode: TreeWalkMode,
    roots: Vec<PathBuf>
}
fn tree_walk_cmd(template: &(TreeWalkMode, &'static str, &'static str))
    -> impl Parser<Args>
{
    let &(mode, name, help) = template;
    let mode = pure(mode);
    let roots = positional::<PathBuf>("ROOTS")
        .help("Files and directories to process")
        .some("Must have at least one file or directory to process.");
    construct!(Args { mode, roots })
        .to_options()
        .command(name)
        .help(help)
}

pub(crate) fn parser() -> OptionParser<Args> {
    let tree_walk_commands = &[
        (TreeWalkMode::Verify, "verify", "..."),
        (TreeWalkMode::Update, "update", "..."),
        (TreeWalkMode::UpdateModified, "update-modified", "..."),
        (TreeWalkMode::Recalculate, "", "..."),
        (TreeWalkMode::Remove, "update-modified", "..."),
    ];
    tree_walk_commands.into_iter()
        .map(tree_walk_cmd)
        .collect::<ParseAlternatives>()
        .to_options()
}

But there is no ParseAlternatives, and the array form of construct! only takes an array literal all of whose elements are identifiers, so to use construct! I would have to write this instead...

pub(crate) fn parser() -> OptionParser<Args> {
    let m_verify = tree_walk_cmd(...);
    let m_update = tree_walk_cmd(...);
    let m_update_modified = tree_walk_cmd(...);
    let m_recalc = tree_walk_cmd(...);
    let m_remove = tree_walk_cmd(...);
   construct!([m_verify, m_update, m_update_modified, m_recalculate, m_remove])
       .to_options()
}

which is unacceptably repetitive. (The documentation says you can use function calls in a construct!([...]) list but when I tried that I got a macro expansion failure -- does it maybe only work for functions that take no arguments?)

I did find a hack that works:

    tree_walk_commands.into_iter()
        .map(tree_walk_cmd)
        .fold(None, |a: Option<Box<dyn Parser<Args>>>, c| {
                #[allow(deprecated)]
                if let Some(cs) = a {
                    Some(Box::new(cs.or_else(c)))
                } else {
                    Some(Box::new(c))
                }
            })
    .to_options()

... but this requires use of the deprecated .or_else method, and also I think it might be double-boxing the parsers.

Please add a supported method for constructing a "parallel composition" of parsers from a slice or an iterator over impl Parser<T>.

(Note that the parallel composition example a_or_b on https://docs.rs/bpaf/latest/bpaf/macro.construct.html still talks about .or_else as if it's OK to use. That's how I knew about it.)

(In case you're wondering, this setup doesn't use positional<TreeWalkMode>("MODE") for two reasons: there's a second set of subcommands that take different arguments, and I want the mode argument to be accepted only before --, which Just Works with command but I don't see any way to do it with positional.)

I'm thinking about making some changes to or_else so it's deprecated just in case. You can write something like this to fold a dynamic collection:

fn choose<T>(xs: Vec<Box<dyn Parser<T> + 'static>>) -> Box<dyn Parser<T>>
where
    T: 'static,
{
    let mut items = xs.into_iter();
    let mut res = items.next().unwrap();
    for next in items {
        res = construct!([res, next]).boxed()
    }
    res
}

Some double boxing is required but it works fast in my experiments.

Please add a supported method for constructing a "parallel composition" of parsers from a slice or an iterator over impl Parser.

Will look into that for the next version.

Thanks for the suggested workaround! Absolutely no hurry.

Absolutely no hurry.

Well, replacement is this, the only difference with suggested workaround is that it does not double box a few things.

fn choice<T: 'static>(xs: impl IntoIterator<Item = Box<dyn Parser<T>>>) -> impl Parser<T> {
    let mut xs = xs.into_iter();
    let mut parser = match xs.next() {
        None => return fail("Invalid choice usage").boxed(),
        Some(p) => p,
    };

    for next in xs {
        parser = Box::new(ParseOrElse {
            this: parser,
            that: next.boxed(),
        })
    }
    parser
}

Will release with the next version. Currently rewriting docs :)

            that: next.boxed(),

Isn't this double-boxing, since the iterator is already yielding Boxes?

Isn't this double-boxing, since the iterator is already yielding Boxes?

Good catch. Indeed it's double boxing. Fixed :)

0.9.6 is out, choice is available as a top level function.