4 ms·
Can this just be done as a lambda that is immediately evaluated? It's just much more verbose. let x = (|| -> Result<i32, std::num::ParseIntError> {
by loeg 11mo ago
Can this just be done as a lambda that is immediately evaluated? It's just much more verbose.
let x = (|| -> Result<i32, std::num::ParseIntError> {
Ok("1".parse::<i32>()?
+ "2".parse::<i32>()?
+ "3".parse::<i32>()?)
})();
- saghm 11mo agoMy instinct is this would get hairy much faster if you want to actually close over variables compared to using a block.
- loeg 11mo agoIf the verbose return type syntax can't be elided, I think it's more or less dead as a pattern.
- ahartmetz 11mo agoNot sure if that is relevant to your point, but: For better and for worse, closing over any outer scope variables is syntactically free in Rust lambdas. You just access them.
- rendaw 11mo agoThat 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.
- ahartmetz 11mo agoIt sounds like you're fighting the language - Rust is sort of FP-light and you're encouraged to return a null/error value from the intermediate calculation instead of doing an early return from the outer scope. It's a nice and easy to follow way to structure the code IME. Yes, it's more verbose when an early return would have been just right - so be it.
- dzaima 11mo agoFor the case where `try` is useful over the functional form (i.e. parent's situation of having a desired Result, plus some unrelated early-returning), that ends up with nested `Result`s though, i.e. spamming an `Ok(Ok(x))` on all the non-erroring cases, which gets ugly fast.
- skribanto 11mo agoWhy couldnt you flatten it?
- dzaima 11mo agoYou have three different value cases (main value, main Err case for `?` to consume, and whatever early-return case). And the `?` operator fully taking up the Err result case means your main-result+early-return values strictly must both be wrapped in an Ok.
- schneems 11mo agoWouldn't that also move any referenced variables too? Unlike the block example that would make this code not identical to what it's replacing.
- pflanze 11mo agoNo, unless you ask for it via the `move` keyword in front of the closure. This works fine: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=cd9fd7df9a309291e48fbd7ac1cf83a3 https://play.rust-lang.org/?version=stable&mode=debug&editio...
- schneems 10mo agoThanks. I was a genuine question and you answered it well. For some reason I've internalized that closures can capture variables sometimes, but I guess I'm not sure the conditions in which that's true (or perhaps I've learned/mis-remembered the wrong lesson a long time ago.
- pflanze 10mo agoIf the code in the closure moves a value, then the closure becomes an FnOnce and does move the value out of the context. That's what you probably have in mind. So, Rust does support partially-moving closures, and does so automatically. But the converse is also true: Rust does not move values just because. If the code inside the closure doesn't move the context, then it will only move context if you use the `move` keyword (in which case the lifetime of the closure is not restricted by borrows of the context; Rust doesn't automatically move things to satisfy the closure's required lifetime, that's a manual decision process consistent with how Rust behaves in other places). There's still one difference to the macro case: the closure borrows all the values at once. So if it needs &mut access to some variable in the context, you can't take another &mut to the same variable outside the closure as long as it is alive (as determined per what the non-lexical lifetime borrow checker can do). Those cases need to be worked around by instead passing in the &mut as a closure argument. Code deposited by macros is not subject to bundling all captures together, hence the borrow checker has more allowance to reduce the borrowing time of each borrow.