Throughout my ~1 year of programming in Rust, I have semi-frequently found myself in a situation where I need to know whether a function that would otherwise return nothing/() succeeded or failed, but not why it succeeded or failed.
I could do this one of four obvious ways without using any external crates.
I could use a bool, using true to indicate success and false to indicate failure.
I could use Option<()>, using Some(()) to indicate success and None to indicate failure.
I could use Result<(), ()>, using Ok(()) to indicate success and Err(()) to indicate failure.
I could use a custom enum, using a Success and Failure variant.
I do not think any of these solutions are sufficient. I have listed my reasoning for disliking each of these methods below (in their respective order).
bools do not carry the requisite semantic meaning, making my code much less-readable.
Option<()> is somewhat of an antipattern and communicates the wrong semantic meaning. "There could be some nothing, or nothing here" is basically nonsensical.
Result<(), ()> is also an antipattern in my opinion, as Results are explicitly meant to be used for enumerated error types, except in the case of Infallible. In addition to this, it prevents you from using ? for Option<T> returns, or for a Result type with different generic parameters (which is pretty much all of them).
Custom enums lack standardization if I were to expose this type in a public interface, and they also prevent me from using the ? operator entirely.
The Solution
I think that std should implement the following type (names are tentative):
enum Status {
Success,
Failure,
}
The reason why I think that this should be in std, and not just some 3rd-party crate, is that it would allow Rustc to support the ? operator as a lossy conversion for both Option<T> and Result<T, E> in a function which returns Status. Without implementation into the language, the only way I could see this being possible is by using procedural macros to tag functions, which tends to have a major negative impact on compile times, and can often cause problems with Cargo check/RA/Cargo clippy.
Why?
Enumerated error types are great, but they are not always necessary.
Some errors cannot accurately be classified, or at least not usefully. This is especially the case when it comes to interacting with external systems and certain FFI systems.
The current non-enumerated error type does not have the correct semantic meaning to be used for this problem.
Some crates end up using something like anyhow because their error types are not easily-enumerable, and in some of these cases it's because the crate is basic and the developer does not feel that their users will have any need for robust error types. Though it is generally discouraged for crates to lack error handling in this way, it seems like something of a "harm reduction" (sorry for the loaded term here) method to prevent careless crate developers from forcing their consumers to use anyhow.
Not every crate is a library. Binary developers should have better flexibility as to how they handle errors, as it is not their responsibility to provide a good interface to other programmers - only to themselves and their teams.
This would help to break the tendency of Option<T> and Result<T, E> types to be close to mutually-exclusive in a project due to poor ergonomics. Though it's hard to communicate this feeling exactly, I suspect that most readers will understand what I mean.
Though I would say this tentatively at best, as someone who has never contributed to std, this might be a way to provide an obvious error return for functions such as Vec::insert() for which reasonable and common error cases will cause a panic. This is based on my understanding that one of the main reasons that a panic is used in place of a Result is due to ergonomic concerns. I may be completely incorrect in this regard, so feel free to correct me.
I am happy to hear your feedback on this, please let me know if you have any questions.
This is actually quite similar to ExitCode, although that has whole-process connotations that you maybe don't care about. Perhaps you could provide a couple motivating examples, and expand upon why this particularly needs to be in std, and can't be locally defined or at least in its own crate.
I agree, unfortunately (as you mentioned) it carries too much baggage related to processes, and additionally would still lack support for the ? operator in conversions.
I did not mean by "enumerated" that it must be an enum, only that it must have some way of representing variants, which is often done with a struct that stores a variant enum in a kind field.
I'm not sure exactly what you mean by this. My point was that the Status enum would be removing any of the data carried by the Result.
Perhaps you could provide a couple motivating examples, and expand upon why this particularly needs to be in std, and can't be locally defined or at least in its own crate.
The custom unit struct has the advantage over Result<(), ()> that you can implement From<MyOperationFailure> for other error types to make using ? easier.
Also, for any Result<_, _> you can write fn_returning_result().ok()? to use it for Option<T> returns.
If it makes your code easier to read for you, you can then emulate your proposed enum Status with
type Status = Result<(), MyOperationFailure>;
const SUCCESS: Status = Ok(());
const FAILURE: Status = Err(MyOperationFailure {});
without any additions to the standard library and without restricting the use of ? for consumers.
I think it's a bit too late for this, for_each has been stable for a long time and is not going away. There's also an argument to be made that some iterators may be able to implement for_each in a faster way due to not having to support early exit.
For this () doesn't need to implement Try, it only needs FromResidual<Option<Infallible>>.
Point 1 I was thinking try_* could go away, but I agree it's probably more work than it's worth. For more efficient implementation you could use specialization if it ever stabilizes.
Frame challenge: I think you're mistaken when you say you don't need to know why something failed. Please provide concrete examples of cases where, in your opinion, the root cause of the failure is unimportant, so we can assess them from our own perspectives.
(Full disclosure: my perspective involves going on 30 years of trying to troubleshoot programs written by other people who didn't make their error reporting as detailed as it could have been.)
I thought implementing Try on () was a cool idea, until someone pointed out that it makes println!()? compile, even tough it doesn't do anything. That'd be super misleading. It will need ok-wrapping magic instead.
Result<(), ()> being an antipattern is news to me. Well, it is indeed an antipattern, but the actual antipattern is not giving detailed errors. So your proposed enum is as much of an antipattern as Result<(), ()>. Type alias it to Status and you're good to go in the cases where you truly don't care (which is very rare!).