Rust’s Block Pattern
43 points by ucirello
43 points by ucirello
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(|| ...)
};
}
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.
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 } {}
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?
A similar pattern is pretty common in Zig code to minimize the scope of variables, and to avoid mutation.
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.
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.
Yup, here's a production example in Meilisearch: https://github.com/meilisearch/meilisearch/blob/9db2b16eedd2fb476fcb7f92b46ec97564c9309a/crates/meilisearch/src/routes/multi_search.rs#L173
We use it to compute a result with context information (query index)
I think we have others, but cannot find them at the moment.
Its kinda a less great implementation of try blocks: https://doc.rust-lang.org/beta/unstable-book/language-features/try-blocks.html
While the tracking issue have been open for close to a decade now it seems like there is some more work on making it fully work recently.
We use it as such in this example yes, but the tool is more general. See other use cases: https://github.com/meilisearch/meilisearch/blob/9db2b16eedd2fb476fcb7f92b46ec97564c9309a/crates/milli/src/vector/parsed_vectors.rs#L404, https://github.com/meilisearch/meilisearch/blob/9db2b16eedd2fb476fcb7f92b46ec97564c9309a/crates/index-scheduler/src/scheduler/create_batch.rs#L654, or https://github.com/meilisearch/meilisearch/blob/9db2b16eedd2fb476fcb7f92b46ec97564c9309a/crates/milli/src/update/new/ref_cell_ext.rs#L20 (ok that one could have been a return I guess 😅)
To emulate try blocks we sometimes use immediately invoked lambdas: https://github.com/meilisearch/meilisearch/blob/9db2b16eedd2fb476fcb7f92b46ec97564c9309a/crates/index-scheduler/src/scheduler/process_batch.rs#L207
Or even an async block: https://github.com/meilisearch/meilisearch/blob/9db2b16eedd2fb476fcb7f92b46ec97564c9309a/crates/meilisearch/src/routes/multi_search.rs#L237. (Ok that one should probably be its own function?)
Which is what I'm not in a hurry to get try blocks. We can already emulate them when they are needed (which is rare)