3 ms·
... only after they ship try_blocks! https://doc.rust-lang.org/nightly/unstable-book/language-features/try-blocks.html https://doc.rust-lang.org/nightly/unstab
by octoberfranklin 6y ago
... only after they ship try_blocks!
https://doc.rust-lang.org/nightly/unstable-book/language-features/try-blocks.html https://doc.rust-lang.org/nightly/unstable-book/language-fea...
Seriously, otherwise you can't do error handling within the body of a function that doesn't return Result<>. The language shouldn't force refactoring decisions on the programmer like that.
- zozbot234 6y ago> Seriously, otherwise you can't do error handling within the body of a function that doesn't return Result<>. Sure you can, just use an inner closure that does return Result<>, and use `match` or the like for error handling. It's not any more verbose than this `try {} ... catch {}` special syntax.
- octoberfranklin 6y ago> It's not any more verbose Seriously? I think this is way less readable: (|| -> Result<_,_> { bar(foo()?) })() than this: try { bar(foo()?)? } Also try_blocks don't have to have a "catch". It's just five characters: "t" "r" "y" "{" "}". I don't see a need for "catch" either.
- Arnavion 6y agoThe closure version would actually be (|| -> Result<_,_> { Ok(bar(foo()?)?) })() to be equivalent, since bar's Err type might need a From::from conversion via `?`. So the try-blocks version is even better in comparison. That said, if you include the `-> Result<_,_>` annotation in the closure case, you also need to include the `: Result<_,_>` annotation for the binding that the try-expr is being assigned to, otherwise the compiler won't know which impl of `std::ops::Try` to use. Of course, you can also use an annotation on the binding in the closure case and get rid of the annotation entirely. That is, the comparison is of: let r: Result<_, _> = (|| Ok(bar(foo()?)?))(); vs let r: Result<_, _> = try { bar(foo()?)? };
- octoberfranklin 6y agoI still find the second case far more readable. When you see "try{}" it says "this is here because I want to turn on ?-handling within this block". When you see a closure formed and then immediately applied it says... well I dunno. Frankly it seems to say "this expression should be simplifiable". In fact it's a huge wart on Rust that beta-reduction doesn't preserve well-formedness of expressions. Also if there were more stuff in the expression body it might not even be obvious at a glance that the closure is being formed-and-then-immediately-invoked, leading a reviewer to wonder "wtf is a closure doing here?".
- Arnavion 6y agoOh yes, my point was not that the try-blocks version was less readable. I agree that it's better and used to use it back when it was still called `do catch { }` and didn't do automatic-Ok-wrapping. I was just making the comparison fairer.
- Arnavion 6y agoThat may work for most cases (and is what I myself often do), but it's worth keeping in mind that it won't work in general. There are many situations where closures are not a replacement for imperative blocks, because the compiler has an understanding of exclusive paths in imperative control flow that does not apply to closures. Eg: fn foo(cond: bool, s: String) { if cond { drop(s); } else { drop(s); } } ... compiles file even though `s` is being consumed in two different locations, because the compiler is aware of the control flow of if-then-else so it knows only one arm can be executed. If you tried to replace the bodies of the two branches with closures, it would not compile, because both need to consume the String. That is, fn if_(cond: bool, t: impl FnOnce(), f: impl FnOnce()) { if cond { t() } else { f() } } fn foo(cond: bool, s: String) { if_(cond, || drop(s), || drop(s)); } ... won't compile. Even though only one of the two closures will be executed, both closures need to be created regardless, which means both need to own the String, which is not allowed. This problem came up all the time in pre-async-await futures 0.1 code because async code had to be written with futures combinators that all used closures, so even in cases where the programmer knows only one closure will be invoked, they would still need to clone their values so that each closure could own them. Even in regular non-async code, it often comes up with the combinators like `Result::map_err`, like: enum Error { Io { path: PathBuf, inner: std::io::Error }, Deserialize { path: PathBuf, inner: serde_yaml::Error }, } fn read_config(path: PathBuf) -> Result<Config, Error> { let f = std::fs::File::open(&path).map_err(|err| Error::Io { path: path.clone(), inner: err })?; let c = serde_yaml::from_reader(&f).map_err(|err| Error::Deserialize { path, inner: err })?; Ok(c) } Notice the `path.clone()` in the first Result's map_err closure, which is needed even though the programmer knows there is nothing else that will use `path` once that statement executes. That's because, once again, both `map_err` closures need to be created regardless of whether they execute or not, so for the second closure to be created it must own `path`, and thus the first closure needs to clone it. The equivalent code with explicit `match` statements for each Result expr would not need to clone. Compare `read_config` and `read_config2` in https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=3effc12ad19b9da8573c4d22fef45bb8 https://play.rust-lang.org/?version=stable&mode=debug&editio...
- zozbot234 6y ago
- setr 6y agoI don't understand; it seems to me try_block just simplifies let result: Result<i32, ParseIntError> = match ("1"::parse<int32>(), "2"::parse<int32>(), "3"::parse<int32>()) { (Ok(x), Ok(y), Ok(z) => Ok(x + y + z), (Err(x), _, _) => Err(x), (_, Err(y), _) => Err(y), (_, _, Err(z)) => Err(z), } ... when the err is the same in every case (or coercible), by providing access to the ? without returning the function in full It doesn't seem to me that it enables any new capability -- just a convenience.
- masklinn 6y agoThey also enable "early exiting" a block, so it's much easier to check for an error in the middle of a block: currently you'd need extra indentation or a `map` / `and_then` (which can lead to the compiler being unhappy as it has more flexibility within a function than across functions). So e.g. let result = { let a = a_thing(); let b = another_thing(); let c = a.foo(b)?; if c.a_thing() { Ok(c) } else { Err(…) } }; which currently has to be written let result = { let a = a_thing(); let b = another_thing(); a.foo(b).and_then(|c| if c.a_thing() { Ok(c) } else { Err(…) } ) } which doesn't look too bad for a simple example but can get out of hand if you need multiple thus, and as mentioned above the compiler can start being an issue if you work across-functions a lot.