Are try blocks being stabilized?
Not that I know of.
Currently in Rust _
is only used for the "don't care" / default pattern in places where patterns are allowed (eg match
exprs), and for inference in type position, especially type arguments (think e.g. the .collect()
call found in FromIterator
, where the turbofish operator is most commonly used).
Even in something like let _ = <EXPR>;
it is essentially the former.
Your proposal suggests inferring values, which at least in general isn't possible. So this would be a special case, which would then lead to needing to explain why it existed in the fist place. The whole thing would just be more trouble than it's worth TBH.
()
is basically a zero-tuple. Since two tuples are the same if their elements are the same, two zero-tuples are always the same, and therefore, the zero-tuple is the Unit.
I'm used to ()
from Scala, except that in Scala, ()
is only the value, and the type is called Unit
.
And of course, you can wrap anything in an Ok
, because that's just an enum
variant that takes a value.
And also in the LHS of assignment expressions; those are a special mix of mostly-an-expression but with some aspects of pattern syntax thrown into it. [But the compiler does consider it an expression, so you can get error messages such as error: in expressions, `_` can only be used on the left-hand side of an assignment
when you try to use _
in expressions anywhere else.]
Yes, that does mean that a weird alternative for getting rid of warning: unused `Result` that must be used
is not to use the compiler suggested
help: use `let _ = ...` to ignore the resulting value
|
6 | let _ = fun_may_error();
| +++++++
but instead do
_ = fun_may_error();
I’ve seen some Rust code do that once; I first thought there might be a mistake, then checked for any warnings at least, and finally had to conclude that the tooling[1] seems to take no issue[2] in this … “shorthand” (it saves 4 characters!![3])
Be careful with using strong words such as "the same". Types having the same sets of elements doesn't mean that the types are the same. It doesn't even make sense to say that the types have "the same elements", because an element is defined only with respect to the type. There is no a priori defined comparison map between any types which would allow you to establish that elements are "the same".
Zero-sized types not being "the same" is the core reason why capability tokens in Rust work. Even if a type is zero-sized, it doesn't mean that you can just construct an element of it out of thin air, without using unsafe code and thus explicitly stepping out of the guarantees of type system. It's also the reason why nominal typing works at all, including Rust's trait system. If all zero-sized types really were "the same", implementing a trait on one of them would implement it on all of them, by equivalence. But again, they are not the same and there is no coercion between them, which allows us to have different sets of implemented traits for different types.
You know, I think the _ = foobar();
pattern is actually not that bad. If I recall correctly, this syntax just wasn't accepted in the past, before a lot of work on destructuring patterns in assignments. For this reason the established idiom is let _ =...;
, but why exactly is it better than _ = ...;
? It's not just about keystrokes, it's also about being visually distinct from normal bindings. We really ignore the returned value, rather than just fail to bind it to something.
With let _ = ...;
, there is an affordance of introducing a named binding in that place. With _ = ...;
, coupled with Rust's strong typing and immutability by default, there is just nothing you can generally write instead of _
to make the code compile. That looks like a stronger signal that keeping the result wasn't ever intended.
Personally the idiom I use instead of let _ = ...;
(which looks weird to me, despite years of working with Rust) is foobar.ok();
. The compiler shuts up, and the idiom reads for me as a confirmation "yes, it's OK to ignore the result, we really don't care about it". But really, it doesn't matter what idiom you use to ignore returned values. The only thing that matters is that you don't do it by accident.
Maybe not types in general, but tuples?
Turbofish, to be precise.
This is incredible. I have no idea what some are even supposed to mean or why they compile.
Tuples are types. Why should there be a difference?
Tuples composed of the same types are indeed the same, but that's a tautology. That's just the way that the generic Tuple<T1, T2, ...>
types is written.
my main concern about let _ = ...
is its significant but silent difference in meaning from let _foo = ...
. I suppose _ = ...
has that over it?
That's fantastic, it made reading this thread worthwhile. Zig uses this syntax for the same purpose, BTW.
The value can be the same, zero-tuple is a single existing value of type ()
/ Unit. When it comes to types we can think in terms of algebraic structure, and then we can say that any two types which have just two values are the same up to an isomorphism. For example Option<()>
is isomorphic to bool
, because we don't lose any information when converting between them.
A functional style language syntax would help but that should be low priority while fleshing out semantics.
I really hope that Rust eventually will get try fn
s together with Ok
-wrapping try
blocks. I don't really care whether it will be try fn foo() -> Result<T, E> { ... }
or fn foo() -> Result<T, E> try { ... }
. The latter is more "consistent" since try
does not influence signature of the function and it can be explained as a simple shortening of { try { ... } }
to try { ... }
, but the former is more "natural" and easier to read in my opinion.
I think it's a very natural feature which will remove a relatively small, but extremely frequent papercut. It integrates really well with the ?
operator and the Try
trait. People who argue that we should "suck it up" sound to me a bit like defenders of the Go's if err != nil { ... }
abomination.
Unfortunately, I think a lot of traction was lost by the fn foo() -> T throws E { ... }
proposals which have derailed the discussion around this feature.
Except that they have precisely nothing to do with each other. They are not nearly comparable.
Go's error handling is:
- a special case in the language; for some operations (eg. type assertion), if you forget the
if err != nil
dance, you magically get a panic. - Repetitive: it adds boilerplate at every place it's used. Every single time you want to handle errors, you need to do
if err != nil
.
Rust has none of these problems. Even before the ?
operator, Rust already had the try
macro, which already abstracted away the repetitive check-and-return pattern. Rust also never silently converts errors to panics. And the Ok(())
at the end of a function:
- is just a regular value, consistent with all other values
- needs to be written only once, not every single time you use a fallible operation
So, no, the "problems" (there's no real problem in Rust) are incommensurate.
Could you give an example of how your proposed try fn's
would look? Along with the usual Rust code they would replace?
I'm having a hard time imagining what mighty be missing from error handling in Rust.
To tide people over, since it seems to boil down to aesthetics, how about:
macro_rules! ok {
() => {
Ok(())
};
($ret:expr) => {
Ok($ret)
};
}
Example playground
Ultimately it saves one character, but maybe it doesn't "look so weird"?
ok!()
}
Vs
Ok(())
}
If there are no "problems" in this area, we wouldn't had numerous proposals to fix them. I explicitly acknowledged that it's a relatively small papercut, but it's extreme frequency warrants solution for it in my opinion.
I vaguely remember that some people used similar arguments against introduction of the ?
operator to replace todo!
: there is no "problem", it's already solved by the macro, no need to introduce this weird language magic, it's just small papercuts, "suck it up", we always used todo!
and will continue to do so, etc.
And the problem is not only about Ok(())
, it's also about numerous Ok(res)
and Err(err)
.
There is a bunch of bikeshedding to do, so do not mind the exact syntax in the examples below. Also, the examples are just random stuff out of my head, so they probably not the best demonstration.
How we write code currently:
fn div(a: &mut u32, b: u32) -> Result<(), Error> {
if b == 0 {
return Err(Error::DivByZero);
}
*a /= b;
Ok(())
}
fn read_u32(f: &mut File) -> io::Result<u32> {
let mut buf = [0u8; 4];
buf.read_exact(&mut buf)?;
let val = u32::from_le_bytes(buf);
if val > i32::MAX as u32 {
let err = io::Error::new(io::ErrorKind::InvalidData, "too big");
return Err(err)
}
if val % 2 == 0 {
return process_odd(f, val);
}
// do non-failing stuff
Ok(val)
}
fn process_u32(a: u32) -> Some(u64) {
if a > 142 {
return None;
}
let c = do_something(a)?;
// a bunch of code which does not return None
let res = calc_res(c);
Some(res)
}
How it could look like with try fn
s:
// `try fn`s must return types which implement the `Try` trait
try fn div(a: &mut u32, b: u32) -> Result<(), Error> {
if b == 0 {
// I will use `fail` in this examples instead of `yeet`,
// some people prefer `throw` instead
fail Error::DivByZero;
}
*a /= b;
}
try fn read_u32(f: &mut File) -> io::Result<u32> {
let mut buf = [0u8; 4];
buf.read_exact(&mut buf)?;
let val = u32::from_le_bytes(buf);
if val > i32::MAX as u32 {
fail io::Error::new(io::ErrorKind::InvalidData, "too big");
}
if val % 2 == 0 {
// Note that `return` performs `Ok`-wrapping
return process_odd(f, val)?;
}
// do non-failing stuff
val
}
try fn process_u32(a: u32) -> Some(u64) {
if a > 142 {
fail;
}
let c = do_something(a)?;
// a bunch of code which does not return None
calc_res(c)
}
try fn
s allow us to concentrate on the "success" path in our code. All possible failure branches can be found by grepping code for ?
and fail
. I am not 100% sure about Ok
-wrapping behavior of return, it could be worth to introduce a separate keyword for "success" return instead which would also work with try
blocks.