Destructuring
When implementing draw_sprite, we need to iterate over each row of a sprite and know two things simultaneously: the row index (to calculate the y position on screen) and the byte value (to extract the pixels). In many languages, you would get one and calculate the other. In Rust, you get both at once — directly in the for loop header — through destructuring.
Destructuring is the ability to unpack a value into its component parts in a single step. It works wherever a value is bound to a name: let statements, for loops, function parameters, and match arms.
Destructuring tuples in let
The most common form — pulling apart a tuple returned by a function:
#![allow(unused)] fn main() { let (result, carry) = self.v[x].overflowing_add(self.v[y]); }
overflowing_add returns (u8, bool). Without destructuring, you would write:
#![allow(unused)] fn main() { let pair = self.v[x].overflowing_add(self.v[y]); let result = pair.0; let carry = pair.1; }
Three lines instead of one, and .0 / .1 tell you nothing about what the values mean. Destructuring names them at the point of creation.
Destructuring in for loops
#![allow(unused)] fn main() { for (row, &byte) in sprite.iter().enumerate() { // row: usize — the row index (0, 1, 2, ...) // byte: u8 — the sprite byte for this row } }
Two things are happening at once here:
Tuple destructuring — .enumerate() yields (usize, &u8) tuples. The (row, &byte) pattern unpacks that tuple directly in the loop header.
Reference pattern — .iter() yields references (&u8). Writing &byte in the pattern matches the reference and binds byte as a plain u8 — the dereference happens in the pattern, not in the loop body. Without it:
#![allow(unused)] fn main() { for (row, byte) in sprite.iter().enumerate() { let b = *byte; // byte is &u8 — manual dereference needed every time } }
The &byte pattern is an idiom worth getting comfortable with — it appears in any loop over a borrowed collection where you want the value rather than the reference.
Destructuring in match
Match arms can destructure the matched value:
#![allow(unused)] fn main() { match point { (0, 0) => println!("origin"), (x, 0) => println!("on x-axis at {}", x), (0, y) => println!("on y-axis at {}", y), (x, y) => println!("at ({}, {})", x, y), } }
The variables x and y in the arms are bound from the matched tuple — no separate extraction step needed.
Ignoring parts with _
#![allow(unused)] fn main() { let (value, _) = some_tuple; // we only need the first element }
_ discards a part without binding it. The compiler also suppresses unused variable warnings for anything prefixed with _.
In the emulator: The
for (row, &byte)pattern is used indraw_spriteto iterate over sprite rows. See The Display for the full implementation and DRW — Drawing Sprites for the opcode that calls it. Thelet (result, carry) = overflowing_add(...)pattern is used in 8XY* — Arithmetic and Logic.