The only other C-family language with that syntax (dot followed by keyword) I know of is Java (where you can write foobarbaz.class; "class" is a language keyword) and it always looked weird.
The Rust maintainers found a great use for it: getting chaining (foo.await?.bar()?.await...) without adding a new postfix "sigil".
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.
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.
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. (|| -> 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()?)? };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?".
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&editio... 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 fullIt doesn't seem to me that it enables any new capability -- just a convenience.
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.