But what's with the Result<(), Error>? Obviously the function can fail, so the Error makes sense, but why is the Rust standard library using a Result for a function which clearly produces no output value?
The description for Result even says that:
It is an enum with the variants, Ok(T), representing success and containing a value, and Err(E) (...)
Yet our result contains no value!
That's exactly why () exists, to represent "no value". Some types exist not because they contain useful information, but because they are the information themselves. Take the ! type for instance. It's even less useful in practice than (). ! is a type with no possible values. Not only does it not contain any information, but you can't even instantiate and return one at all. The fact that it's impossible to do so is actually why ! is useful at all, () means "nothing", while ! means "impossible". Result<u16, !> conveys that this function will always succeed, even when a Result is required (e.g. trait impls, more on that later). Similarly, Result<(), E> conveys that the function might succeed or fail, and that it returns nothing on success. Just because a type is present doesn't mean that type needs to (or even should) carry information at runtime.
To put it succinctly, () and ! convey information to the programmer during development, rather than conveying information to the function calling them at runtime.
A better definition might use Option instead...
While we do agree that it isn't a good solution, my reasoning differs. For me, it's not the semantics of the code itself, but the fact that Option conveys different meaning than Result. Result is clearly tied to success and failure, whereas Option is not. Returning a Result clearly signals "This might work, it might not", and having that readability is so important to me that oftentimes I'll even use Result<(), ()> instead of just bool. Looking at a function signature and seeing that it returns Result is an instant clue to me that this function fails. Seeing Option<Error> wouldn't be as clear to me. The try syntax is just a bonus compared to the readability problems.
It fills the gap left by Option and Result by providing a type that signifies either the successful completion of an operation or an error.
This might sound a bit harsh, but there really isn't a gap there. Result<(), E> does that already.
If I were to be a little provocative, I would perhaps ask why Option exists, when its use cases could just as easily be covered by Result<T, ()>.
Same thing as the Option<Error> bit, just reversed. Option tells the programmer "this function might return something, but it might not", whereas Result tells the programmer "this function might succeed, and it might fail". Opening your fridge and seeing no food in there isn't an error, your fridge isn't broken or anything, and you didn't fail to look for food, it's just empty. Result doesn't make sense, even if semantically it carries the same value.
but I would argue that clearly communicating intent when designing the signatures of your functions is an absolute readability win.
How is Fallible<E> clearer than Result<(), E>? The latter is just as clear if not clearer than the former, they both convey "this might succeed, and return nothing, or fail, and return E".
95% of use cases can be covered by Result and Option
What's the 5% here? I can't think of anything where Fallible<E> would work but Result<(), E> wouldn't.
It's been said that perfection is achieved when there's nothing left to take away, and that feckin' () needs to go!
I'd say the exact opposite. This crate adds a whole new type to represent something that can already be represented by another type, and it splits "might succeed, might fail" into two types that can't intermingle instead of having two different variants of the same type. To me, that seems like taking something simple and making it more complex.
One other thing that's also worth noting, even if your crate saw 100% adoption, Result<(), E> wouldn't be entirely eliminated.
trait Flammable<T> {
fn try_burn(&mut self) -> Result<T, Error>;
}
// wood turns into charcoal when burnt
impl Flammable<Charcoal> for Wood {
fn try_burn(&mut self) -> Result<Charcoal, Error> {
if self.wet {
return Err(Error::Wet);
}
Ok(Charcoal::new())
}
}
// paper burns cleanly and leaves nothing behind
impl Flammable<()> for Paper {
// even though we return no value here, we can't use Fallible
// because the trait requires Result. since Wood still requires
// a value in the successful case, we can't change the trait to
// use Fallible either.
fn try_burn(&mut self) -> Result<(), Error> {
if self.wet {
return Err(Error::Wet);
}
Ok(())
}
}
There's no way to replace Result<(), E> with Fallible<E> without making two traits for things that burn cleanly and things that burn and produce a result.
To sum up my thoughts, this crate seems a bit misguided. I think it fills a niche that I'm not sure exists. Splitting Result into 2 types seems like a hefty price to pay for the small benefit of not seeing or typing () some of the time. It's worse for readability, usability, and maintainability, with no real benefit. I don't really think Result<(), E> is unclear, it conveys exactly what it means, so there's not really much for this crate to fix.
That's exactly why () exists, to represent "no value". Some types exist not because they contain useful information, but because they are the information themselves. Take the
!type for instance. It's even less useful in practice than().!is a type with no possible values. Not only does it not contain any information, but you can't even instantiate and return one at all. The fact that it's impossible to do so is actually why!is useful at all,()means "nothing", while!means "impossible".Result<u16, !>conveys that this function will always succeed, even when a Result is required (e.g. trait impls, more on that later). Similarly, Result<(), E> conveys that the function might succeed or fail, and that it returns nothing on success. Just because a type is present doesn't mean that type needs to (or even should) carry information at runtime.To put it succinctly,
()and!convey information to the programmer during development, rather than conveying information to the function calling them at runtime.While we do agree that it isn't a good solution, my reasoning differs. For me, it's not the semantics of the code itself, but the fact that Option conveys different meaning than Result. Result is clearly tied to success and failure, whereas Option is not. Returning a Result clearly signals "This might work, it might not", and having that readability is so important to me that oftentimes I'll even use
Result<(), ()>instead of justbool. Looking at a function signature and seeing that it returnsResultis an instant clue to me that this function fails. SeeingOption<Error>wouldn't be as clear to me. The try syntax is just a bonus compared to the readability problems.This might sound a bit harsh, but there really isn't a gap there.
Result<(), E>does that already.Same thing as the
Option<Error>bit, just reversed.Optiontells the programmer "this function might return something, but it might not", whereasResulttells the programmer "this function might succeed, and it might fail". Opening your fridge and seeing no food in there isn't an error, your fridge isn't broken or anything, and you didn't fail to look for food, it's just empty. Result doesn't make sense, even if semantically it carries the same value.How is
Fallible<E>clearer thanResult<(), E>? The latter is just as clear if not clearer than the former, they both convey "this might succeed, and return nothing, or fail, and return E".What's the 5% here? I can't think of anything where
Fallible<E>would work butResult<(), E>wouldn't.I'd say the exact opposite. This crate adds a whole new type to represent something that can already be represented by another type, and it splits "might succeed, might fail" into two types that can't intermingle instead of having two different variants of the same type. To me, that seems like taking something simple and making it more complex.
One other thing that's also worth noting, even if your crate saw 100% adoption,
Result<(), E>wouldn't be entirely eliminated.There's no way to replace
Result<(), E>withFallible<E>without making two traits for things that burn cleanly and things that burn and produce a result.To sum up my thoughts, this crate seems a bit misguided. I think it fills a niche that I'm not sure exists. Splitting Result into 2 types seems like a hefty price to pay for the small benefit of not seeing or typing
()some of the time. It's worse for readability, usability, and maintainability, with no real benefit. I don't really thinkResult<(), E>is unclear, it conveys exactly what it means, so there's not really much for this crate to fix.