Rust’s Block Pattern

43 points by ucirello


dtolnay

One aspect not covered in the article is that this technique is especially relevant when working with function-local const and static items due to how their scope works.

fn demo() {
    // Surprise! STATIC is already in scope here!
    println!("{}", STATIC);

    // Unlike local which is not in scope.
    println!("{}", local);  //[ERROR cannot find value `local` in this scope]

    static STATIC: i32 = 1;
    let local: i32 = 1;

    // Instead of doing this:
    static THING: OnceLock<Thing> = OnceLock::new();
    let thing = THING.get_or_init(|| ...);

    // Endeavor to do this:
    let thing = {
        static THING: OnceLock<Thing> = OnceLock::new();
        THING.get_or_init(|| ...)
    };
}

Real-world example from my own code.

spookylukey

This is similar to "where" clauses in Haskell that let you define helpers at the bottom of your code, so that you can start with the main body of what you want to do, and leave the details to later. I've often wanted that in other languages.

timvisee

It's a great pattern to use together with lock guards. You can intuitively hold a lock within the scope of a block. I tend to use them for code that doesn't deserve extracting to a dedicated function.

Another interesting use case is a do-while style loop: while { code } {}

eta

Interesting! Personally I only do this when the borrow checker is forcing me to (for example if I want to lock something and then release the lock before doing another thing), so I hadn't thought of doing it otherwise; I wonder whether other people reading code written like this might assume there's some reason the block exists beyond just stylistic?

dryya

I like it! I agree with the post's aside that you should begin with the top-level function so the intent and purpose of what you're doing is clear, and this pattern seems like a logical extension to within individual functions.

valdemar

One interesting thing that is not listed in this post is that it is possible to break out of a block akin to a early return, and because of that you can also have named blocks.

fn main() {
    let outer = |x| 'outer: {
        let mut a = x;
        let b = {
            if a == 5 {
                break 'outer 1234;
            }
            a += 1;
            a
        };
        a
    };
    for i in 0..=5 {
        println!("{}", outer(i));
    }
}

So you can kinda use blocks as scoped early returns which is a bit funky and not something I have really used in practice.