1

I have a nested loop where I want to check if a certain condition holds on a table of usize values. However, the table may only be partially filled -- so it is actually a table of Option<usize> variables. When at least one of the values is missing, I do not consider the condition to be violated, and therefore want to continue on with the next set of values.

Currently I do it as follows:

for i in 0..n {
    for j in 0..n {
        for k in 0..n {
            let v_ij = match table[i][j] {
                None => { continue; },
                Some(val) => { val }
            };

            let v_jk = match table[j][k] {
                None => { continue; },
                Some(val) => { val }
            };

            let res_left = match table[v_ij][k] {
                None => { continue; },
                Some(val) => { val }
            };

            let res_right = match table[i][v_jk] {
                None => { continue; },
                Some(val) => { val }
            };

            if res_left != res_right {
                return false;
            }
        }
    }
}

(For context: the computation is checking whether a partial magma is associative, but this is not important for the question.)

Is there an idiomatic way to replace all these matches? In Haskell, using the Maybe monad, I believe I could wrap this piece of code up into a do block, so that None values are automatically propagated. I tried using table[i][j].unwrap_or({continue;}) but this evaluates the continue greedily; using table[i][j].unwrap_or_else({continue;}) gives a syntax error.

Mees de Vries
  • 639
  • 3
  • 24
  • `match (a, b, c, d, e) { (Some(a), Some(b), ...) => {}, None => {} }` – Stargateur May 01 '21 at 11:53
  • 1
    @Stargateur, that makes it a bit shorter, but note that the third and fourth `match` blocks use the outputs of the first two as indices into `table`. I would still be left with two blocks (and in fact with three because the top block should be one for loop "up"). – Mees de Vries May 01 '21 at 11:58
  • `table[i][j].and_then(|v_ij| table[j][k].and_then(|v_jk| ...` https://doc.rust-lang.org/std/option/enum.Option.html#method.and_then – Stargateur May 01 '21 at 12:02
  • Does this answer your question? [What's the idiomatic way to handle multiple \`Option\` in Rust?](https://stackoverflow.com/questions/50731439/whats-the-idiomatic-way-to-handle-multiple-optiont-in-rust) – Stargateur May 01 '21 at 12:11
  • see also https://stackoverflow.com/questions/55755552/what-is-the-rust-equivalent-to-a-try-catch-statement/55758013#55758013 – Stargateur May 01 '21 at 12:11

2 Answers2

4

Using the ? operator on options to do the continue for you, and itertools to build the iterator, you could take a functional approach:

use itertools::Itertools;

let has_false = (0..n)
    .permutations(3)
    .filter_map(|v| {
        let (i, j, k) = (v[0], v[1], v[2]);
        let v_ij = table[i][j]?;
        let v_jk = table[j][k]?;
        Some((table[v_ij][k]?, table[i][v_jk]?))
    })
    .any(|(res_left, res_right)| res_left != res_right);
Denys Séguret
  • 372,613
  • 87
  • 782
  • 758
2

In such a case, to limit repetition and to improve clarity, I would probably use a macro:

macro_rules! unwrap_or_continue {
    ($opt: expr) => {
        match $opt {
            Some(v) => v,
            None => {continue;}
        }
    }
}

There is loop_unwrap crate that implement something similar but more complex.

Thus each of your test would be reduced:

for i in 0..n {
    for j in 0..n {
        for k in 0..n {
            let v_ij = unwrap_or_continue!(table[i][j]);
            let v_jk = unwrap_or_continue!(table[j][k]);
            let res_left = unwrap_or_continue!(table[v_ij][k]);
            let res_right = unwrap_or_continue!(table[i][v_jk]);
            if res_left != res_right {
                return false;
            }
        }
    }
}

Stargateur
  • 24,473
  • 8
  • 65
  • 91
Denys Séguret
  • 372,613
  • 87
  • 782
  • 758