Reducing code repetition with Vecs and fields

I am working on some code that needs to do nearly identical things on 4 total items. I am looking for a way to simplify this logic, so I don't have 4 nearly identical match statements.

Thanks in advance!

I have a Vec of 2 items, each with 2 fields.

Currently I have 4 separate match statements that all have the exact same code in them, with one difference between the two items in the vec, which of the end1/end2 variables to assign to.

Right now, this is just copy/pasted between match statements, but the code is going to get more complex and I don't want to have to maintain 4 separate copies.

In theory, only 2 of the total 4 match statements should ever actually trigger but I have simplified the code a bit here.

let mut end1 = Pos2;
let mut end2 = Pos2;

let connections: Vec<Connection> = Vec::new();
//Populate connections vec here
// connections could have more than 2 items in it, so match on case where len() == 2

    match connections.len().cmp(&2) {
        Ordering::Equal => {
            match &connections[0].end1 {
                Type1 => {
                    //perform logic here matching on type of connections[0].end1
                    let new_position = Pos2::ZERO;
                    end1 = new_position;
                }
                _ => {
                    todo!()
                }
            }
            match &connections[0].end2 {
                Type1 => {
                    //perform logic here matching on type of connections[0].end1
                    let new_position = Pos2::ZERO;
                    end1 = new_position;
                }
                _ => {
                    todo!()
                }
            }
            match &connections[1].end1 {
                Type1 => {
                    //perform logic here matching on type of connections[0].end1
                    let new_position = Pos2::ZERO;
                    end2 = new_position;
                }
                _ => {
                    todo!()
                }
            }
            match &connections[1].end2 {
                Type1 => {
                    //perform logic here matching on type of connections[0].end1
                    let new_position = Pos2::ZERO;
                    end2 = new_position;
                }
                _ => {
                    todo!()
                }
            }
        }
        Ordering::Less | Ordering::Greater => {
            todo!();
        }
    }

If folks are interested or need more context, the actual code is here, starting at line 123

I don't see why a function wouldn't work.

match connections.len().cmp(&2) {
  Ordering::Equal => {
    match_connection_end(connections[0].end1, &mut end1);
    match_connection_end(connections[0].end2, &mut end1);
    match_connection_end(connections[1].end1, &mut end2);
    match_connection_end(connections[1].end2, &mut end2);
  }
}

If you want to handle more than 2 items, I would recommend rewriting it as a loop.

2 Likes

How's this?

    match connections.len().cmp(&2) {
        Ordering::Equal => {
            let ends = [
                (&mut end1, &connections[0]),
                (&mut end2, &connections[1])
            ];

            for (end, conn) in ends {
                let conn_ends = [ &conn.end1, &conn.end2 ];
                for conn_end in conn_ends {
                    match conn_end {
                        Type1 => {
                            let new_position = Pos2::ZERO;
                            *end = new_position;
                        },
                        _ => todo!()
                    }
                }
            }
        }
        Ordering::Less | Ordering::Greater => {
            todo!();
        }
    }

Marking this as the solution for now, as it is expandable.

Didn't realize I could put references in slices like that.

Thanks

Forgot I could use mutable parameters. Thanks!

you can use slice patterns in this case, to match the length and bind variable to the elements in a single step:

let mut connections: Vec<Connection> = todo!();
// use `&mut connections[..]` if you need `ref mut Connection`
match &connections[..] {
    // example cases

    // for empty slice:
    [] => todo!("empty"),
    // for single element:
    [c0] => {
        // c0: &Connection
        do_something(c0);
    },
    // case for 2 elements
    [c0, c1] => {
        do_something(c0);
        do_something(c1);
    },
    // to split the first 2 elements and the rest,
    // demonstrate the "verbose" syntax without match ergonomics
    // the length is greater-or-equal to 2 in this case
    &[ref c0, ref c1, ref rest@...] => {
        // c0: &Connection; c1: &Connection
        // rest: &[Connection]
    }
    // other cases, or use `_` pattern if don't care the elements in this case
    connections => {
        // connections: &[Connection]
    }
}

see also slice::split_at_checked() for simple cases.

3 Likes

Interesting. Do you think this would be more performant than using len().cmp(&2)?

in theory, using slice pattern could have slightly better performance than comparing len and then using the indexing operator; but at least, it should be not worse.

in rust, the indexing operator (i.e. square brackets) is a safe operation, and each operation includes a boundary check. although this check is cheap, it is not free. in certain cases, it could be optimized away, but it is NOT guaranteed (if possible at all).

using slice pattern only checks the len once, you don't need the indexing operator to access the elements.

this is analog to using the Some(...) pattern to extract a value inside an Option, v.s. checking .is_some() followed by .unwrap():

// single step to check condition and extract contents using refutable pattern
fn foo(x: Option<&i32>, xs: &[i32]) {
    if let Some(x) = x {
        do_something(x);
    }
    if let [x0, x1, ..] = xs {
        do_something_thing(x0);
        do_something_thing(x1);
    }
}

// separate steps to check condition, then to extract contents
fn bar(x: Opiton<&i32>, xs: &[i32]) {
    if x.is_some() {
        let x = x.unwrap();
        do_something(x);
    }
    if xs.len() >= 2 {
        let x0 = &xs[0];
        let x1 = &xs[1];
        do_something(x0);
        do_something(x1);
    }
}

however, the reason to prefer one approach over the other should not be the (practically negligible or non-existet at all) performance difference, but for familiarity, readability, and understandability, of the code, or, as often said, write idiomatic code.


also, a subtle technical difference between slice pattern and checked indexing operator worth mentioning: there are cases where it is not possible to use the indexing operator but ok to use slice pattern, when you deal with exclusive references, a.k.a. &mut Type. in the following example, code in function foo() will be rejected by the borrow checker, while function bar() compiles without error:

fn foo(xs: &mut [i32]) {
    if xs.len() >= 2 {
        let x0 = &mut xs[0];
        let x1 = &mut xs[1];
        do_something(x0, x1);
    }
}

fn bar(xs: &mut [i32]) {
    if let [x0, x1, ..] = xs {
        do_something(x0, x1);
    }
}
4 Likes