Using tuples to declare multiple variables with a single let

This is something I started doing, and I wondered about whether it is "good style" or not. I feel it is probably ok, (clippy doesn't complain!) but you could argue it is slightly more complicated than necessary.

Here is an example:

    fn write_blocks(&mut self, f: &FD, offset: u64, data: Data) {
        let (mut done, len) = (0, data.len());
        while done < len {
            let off = offset + done as u64;
            let (blk, off) = (off / BLK_CAP, off % BLK_CAP);
            let a = min(len - done, (BLK_CAP - off) as usize);
            let blk = self.get_block(f.root, f.level, blk);
            self.0.write_data(blk, off, data.clone(), done, a);
            done += a;
        }
    }

( Taken from here )

What are your feelings on this?

Personally I don't like to see things like:

    let (blk, off) = (off / BLK_CAP, off % BLK_CAP);

Especial if those computations get any more complicated. This just seems like cramming more stuff into a line just for the sake of saving a line. I like to keep lines a simple as possible for clarity.

Except for real simple things like:

    let (x, y) = (y, x);

perhaps.

I think it is fine. I personally do that often when I work with indices. Note that

let (blk, off) = (off / BLK_CAP, off % BLK_CAP);

is such a common pattern that num offers a div_rem function and method you might find useful.

3 Likes

It's something to use with care; as long as the RHS remains easy to read (as in your case), it's fine, but you need to watch out for the RHS becoming complex, where it obfuscates the code.

So, an RHS of (variable / CONSTANT, variable % CONSTANT) is fine - it's simple and easy to read.

An RHS of ((complex + arithmetic * expression) / (complex - computation), (complex - arithmetic * expression) % (complex - computation)) is not, since it makes it hard to see what's going on. After all, if you look at this case, I've introduced an easy-to-miss difference between the tuple elements, but it's obscured by the sheer size of this RHS.

4 Likes

I think in addition to limiting expression size it makes sense to not bundle together less closely related variables. E.g. if blk_cap was computed with self.blk_cap() in place of being a constant in your example you still should not put it into let (mut done, len, blk_cap) even if there is enough space to do so.

Maybe in some cases it makes sense to actually use your proposal to emphasize variables being related even in case of long expressions: if for @farnz example in place of writing everything in a single line you wrote

let (a, b) = (
    (complex + arithmetic * expression) / (complex - computation),
    (complex - arithmetic * expression) % (complex - computation),
);

you get expression which is two lines longer than

let a = (complex + arithmetic * expression) / (complex - computation),
let b = (complex - arithmetic * expression) % (complex - computation),

, but now a and b are viewed as connected and additionally moving b away from a during refactoring is not a matter of simply cutting one line and pasting it in another place. Last, but not least in the example + / - and % / / are naturally aligned which would not happen if a had different variable name length than b.

1 Like

The difficulty with something that depends on formatting is that you then have to stop rustfmt or similar tools from changing the formatting on you. I would prefer to refactor my example to:

let (a, b) = {
    let dividend = complex + arithmetic * expression;
    let divisor = complex - computation;
    (dividend / divisor, dividend % divisor)
};

(albeit with domain-specific names)

This way, it's now impossible for me to change just one of the two halves, and it's no longer sensitive to details of the formatter's configuration.

Again, though, it's a matter of being careful to keep it easy to read, and not hard-and-fast rules. If in doubt, factor into more variables and let Rust handle the "optimization" for you.

1 Like