Do I need to rewrite my nested for_each iteration into a nested for loop if I want to break it within the loop?

Rust playground

I created a "simplified version" of my menu in the playground.
As you can see there's an "install" option that starts
"firing off installation commands".
Trouble is is when there's an error in one of those commands,
it needs to go to show an error box and then go back to the main menu.

I've got issues with this because I've already written the install sequence as
a nested iterator and with so many variables to keep track of (i, j, command_group, command, text), I'm getting lost on how properly write a loop for this thing anymore.

But I do get the feeling I should get rid of it and write a nested for loop instead.

Am I right on this?

You probably need nested loops indeed.

Just know that you can always rewrite iterator.for_each(|value| { body } into a loop:

for value in iterator {
    body
}

In this case this leads to something like this:

    for (i, (text, command_group)) in command_groups.iter().enumerate() {
        let percent = i * 100 / cg_len; 
        println!("Install step: {}\nAt {}%", text, percent);
    
        for (j, command) in command_group.iter().enumerate() {
            match command {
                &"do_command_2_0" => {}, // return Page::InstallError 
                _ => println!("Executing command #{}: {}", j, command),
            }
        }
    }

Now you can return or break from the loop.

You could also use try_for_each so that you can break out of the loop.

Since I'm not sure what you're asking, I'm also giving the tip that you can break out of the outer loop from the inner loop using labeled loops and breaks: Nesting and labels - Rust By Example

2 Likes

There's a point where return Page::InstallError
needs to be called, for example,
when the folder can not be found it needs to install to,
instead of Page::Finish.
So I do a match command.prepare() to see what next step needs to be made,
The 'command' in my example is named option_command in my actual code,
as the option execute zero, one or multiple commands
depending on the result of the prepare method.

match option_command.prepare() {
    Commands::Zero() => {}, 
    Commands::One(Command) => println!("Executing command #{}: {}", j, command),
    Commands::Multiple(commands) => println!("Executing commands #{}: {:?}", j, commands),
    Commands::Error(page) => return page,
}

I can't call return page within a closure as it starts complaining about types not matching,
but apparently I can within a loop.

I can't call return page within a closure as it starts complaining about types not matching,

That's because a return statement is scoped to the closure (which is basically its own function). break and continue won't work for the same reason (though continue; can be emulated with return;.

Here's that with try_for_each:

fn handle_install() -> Page {
    let command_groups = [
        ("text_1", ["do_command_1_0", "do_command_1_1"]),
        ("text_2", ["do_command_2_0", "do_command_2_1"]),
    ];
    let cg_len = command_groups.len();

    let r = command_groups
        .iter()
        .enumerate()
        .try_for_each(|(i, (text, command_group))| {
            let percent = i * 100 / cg_len;
            println!("Install step: {}\nAt {}%", text, percent);

            command_group
                .iter()
                .enumerate()
                .try_for_each(|(j, command)| {
                    match command {
                        &"do_command_2_0" => return Err(Page::InstallError),
                        _ => println!("Executing command #{}: {}", j, command),
                    }
                    Ok(())
                })
        });

    if let Err(e) = r {
        return e;
    }

    Page::Finish
}

This looks better when your error is an actual Result since you can use ? instead of matching it. But the for loop is probably simpler anyway.