Earlier quoted context omitted.
Can this just be done as a lambda that is immediately evaluated? It's just much more verbose. let x = (|| -> Result { Ok("1".parse:: ()? + "2".parse:: ()? + "3".parse:: ()?) })();
That prevents other control flow mechanisms (return, break) from operating past the function boundary. In general, I avoid single-callsite functions as much as possible (including the iterator api) for this reason.
Rust's Block Pattern
71–80 of 121 posts
Re: Rust's Block Pattern
#72I think the technique is important to have in your vocabulary, but I think the examples given are a weak sell. In the example given, I would have preferred to extract to a method—-what if I want to load the config from somewhere else? And perhaps the specific of strip comments itself could have been extracted to a more-semantically-aptly named post-processing method. I see the argument that when extracted to a functi…
There are DRY and WET principles. We can argue which one of them is better, but to move something used exactly once to a method just due to an anxiety you can need it again seems to me a little bit too much. I move things into functions that are called once, but iff it makes my code clearer. It can happen when code is already complicated and long.
The block allows you to localize the code, and refactoring it into a separate function will be trivial. You need not to check if all the variables are temporary, you just see the block, copy/paste it, add a function header, and then add function call at the place where the block was before. No thinking and no research is needed. Veni, vidi, vici.
> The fact that you need to explain what’s happening with comments is a smell.
It is an example for the article taken out of a context. You'd better comment it for the sake of your readers.
> I think blocks are useful when you are referencing a lot of local variables and also have fairly localized meaning within the method.
I do it each time I need a temporary variable. I hate variables that exist but are not used, they make it harder to read the code, you need to track temporaries through all the code to confirm that they are temporaries. So even if I have just two local variables (not "a lot of") and one of them is temporary, I'd probably localize the temporary one even further into its own block. What really matters is a code readability: if the function has just three lines, it doesn't matter, but it becomes really ugly if a lifetime of a variable overshoots its usefulness for 20 lines of a dense code.
The other thing is mutability/immutability: you can drop mutability when returning a value from a block. Mutability makes reasoning harder, so dropping it when you don't need it anymore is a noble deed. It can and will reduce the complexity of reading the code. You'll thank yourself many times later, when faced with necessity to reread your own code.
There is a code and there is the process of devising the code. You cannot understand the former without reverse engineering the latter. So, when you write code, the more of your intentions are encoded somehow in your code, the easier it will be to read your code. If you create temporary variables just to parse config with the final goal to get the parsed config in a variable, then you'd better encode it. You can add comments, like "we need to parse config and for that we need three temporary variables", or you can localize those three temporary variables in a block.
Re: Rust's Block Pattern
#73Earlier quoted context omitted.
Can this just be done as a lambda that is immediately evaluated? It's just much more verbose. let x = (|| -> Result { Ok("1".parse:: ()? + "2".parse:: ()? + "3".parse:: ()?) })();
My instinct is this would get hairy much faster if you want to actually close over variables compared to using a block.
Re: Rust's Block Pattern
#74I have one better: the try block pattern. https://doc.rust-lang.org/beta/unstable-book/language-featur...
Can this just be done as a lambda that is immediately evaluated? It's just much more verbose. let x = (|| -> Result { Ok("1".parse:: ()? + "2".parse:: ()? + "3".parse:: ()?) })();
Re: Rust's Block Pattern
#75Re: Rust's Block Pattern
#76Much of the value of this block pattern is that it makes the scope of the intermediate variables clear, so that you have no doubt that you don’t need to keep them in mind outside that scope.
But it’s also about logical grouping of concepts. And that you can achieve with simple ad hoc indentation:
fn foo(cfg_file: &str) -> anyhow::Result {
// Load the configuration from the file.
// Cached regular expression for stripping comments.
static STRIP_COMMENTS: LazyLock = LazyLock::new(|| {
RegexBuilder::new(r"//.*").multi_line(true).build().expect("regex build failed")
});
// Load the raw bytes of the file.
let raw_data = fs::read(cfg_file)?;
// Convert to a string to the regex can work on it.
let data_string = String::from_utf8(&raw_data)?;
// Strip out all comments.
let stripped_data = STRIP_COMMENTS.replace(&config_string, "");
// Parse as JSON.
let config = serde_json::from_str(&stripped_data)?;
// Do some work based on this data.
send_http_request(&config.url1)?;
send_http_request(&config.url2)?;
send_http_request(&config.url3)?;
Ok(())
}
(Aside: that code is dreadful. None of the inner-level comments are useful, and should be deleted (one of them is even misleading). .multi_line(true) does nothing here (it only changes the meanings of ^ and $; see also .dot_matches_new_line(true)). There is no binding config_string (it was named data_string). String::from_utf8 doesn’t take a reference. fs::read_to_string should have been used instead of fs::read + String::from_utf8. Regex::replace_all was presumably intended.)It might seem odd if you’re not used to it, but I’ve been finding it useful for grouping, especially in languages that aren’t expression-oriented. Tooling may be able to make it foldable, too.
I’ve been making a lightweight markup language for the last few years, and its structure (meaning things like heading levels, lists, &c.) has over time become almost entirely indentation-based. I find it really nice. (AsciiDoc is violently flat. reStructuredText is mostly indented but not with headings. Markdown is mostly flat with painfully bad and footgunny rules around indentation.)
—⁂—
A related issue. You frequently end up with multiple levels of indentation where you really only want one. A simple case I wrote yesterday in Svelte and was bothered by:
$effect(() => {
if (loaded) {
… lots of code …
}
});
In some ancient code styles it might have been written like this instead: $effect(() => { if (loaded) {
… lots of code …
} });
Not the prettiest due to the extra mandatory curlies, but it’s fine, and the structure reasonable. In Rust it’s nicer: effect(|| if loaded {
… lots of code …
});
But rustfmt would insist on returning it to this disappointment: effect(|| {
if loaded {
// … lots of code …
}
});
Perhaps the biggest reason around normalising indentation and brace practice was bugs like the “goto fail” one. I think there’s a different path: make the curly braces mandatory (like Rust does), and have tooling check that matching braces are at the same level of indentation. Then the problem can’t occur. Once that’s taken care of, I really see no reason not to write things more compactly, when you decide it is nicer, which I find quite frequently compared with things like rustfmt.I would like to see people experiment with indentation a bit more.
—⁂—
One related concept from Microsoft: regions. Cleanest in C♯, `#region …` / `#endregion` pragmas which can introduce code folding or outlining or whatever in IDEs.
Re: Rust's Block Pattern
#77I use this all the time. It's features like these that sell Rust for me honestly; even if you wrapped your whole program in `unsafe` it would still be a massively better language than C++ or C.
Re: Rust's Block Pattern
#78I love that this is part of the syntax. I typically use closures to do this in other languages, but the syntax is always so cumbersome. You get the "dog balls" that Douglas Crockford always called them: ``` const config = (() => { const raw_data = ... ... return compiled; })()' const result = config.whatever; // carry on return result; ``` Really wish block were expressions in more languages.
{
const x = 5;
x + 5
}
// => 10
x
// => undefined
But I don’t see a way to get the result out of it. As soon as you try to use it in an expression, it will treat it as an object and fail to parse.Re: Rust's Block Pattern
#79There are some situations with tricky lifetime issues that are almost impossible to write without this pattern. Trying to break code out into functions would force you to name all the types (not even possible for closures) or use generics (which can lead to difficulties specifying all required trait bounds), and `drop()` on its own is of no use since it doesn't effect the lexical lifetimes.
Conversely, I use this "block pattern" a lot, and sometimes it causes lifetime issues: let foo: &[SomeType] = { let mut foo = vec![]; // ... initialize foo ... &foo }; This doesn't work: the memory is owned by the Vec, whose lifetime is tied to the block, so the slice is invalid outside of that block. To be fair, it's probably best to just make foo a Vec, and turn it into a slice where needed.
Re: Rust's Block Pattern
#80We do this via run in TS: export const run = (f: () => T): T => { return f(); };