There may be something I’m missing, but peeking at the docs again,
I’m not seeing fn I’m seeing variant.
I’m pretty sure variant and fn are different things.
–edit: that is: Result::Ok is a variant. it’d be hard to be a fn too.
There may be something I’m missing, but peeking at the docs again,
I’m not seeing fn I’m seeing variant.
I’m pretty sure variant and fn are different things.
–edit: that is: Result::Ok is a variant. it’d be hard to be a fn too.
You can see it if you try to use it like a function:
Compiling playground v0.0.1 (file:///playground)
error[E0308]: mismatched types
--> src/main.rs:3:17
|
3 | let a: () = Result::Ok;
| ^^^^^^^^^^ expected (), found fn item
|
= note: expected type `()`
found type `fn(_) -> std::result::Result<_, _> {std::result::Result<_, _>::Ok}`
Ok
is a variant and a function. A variant is basically a function that constructs an enum: it has special handling (via the discriminant) in order to allow the compiler to destructure the value. That is, you can write if let Ok(a) = result {}
, because the compiler knows Ok
is a simple constructor of Result
. Conversely, you can’t write if let foo(a) = val
, because foo
could execute arbitrary code and there’s no reliable way to guarantee that val
is actually constructable via foo
. Thus enum variants are a special kind of function: one that are extremely simple and uniquely associated with values of the enum type.
Addendum: Incidentally, this kind of understanding is probably one of the big reasons I don’t like the idea of simplifying Ok(())
. When I see Ok(())
, I see something that is a clean and elegant expression of the logic of the program. To replace it with something builtin like pass
or ()
feels to me like a math student proposing that we declare 1,000,000 the highest number. Sure, in some naive sense it probably sounds simple and elegant, but it also relays a fundamental misunderstanding of why higher numbers exist.
All tuple variants of evey enum are functions.
Thanks for clarification.
no problem, you can see this in this example. That both tuple structs and tuple type enum variants are also functions.
enum EnumExample {
Tuple(i32, i32),
}
fn func_1(f: fn(i32, i32) -> EnumExample) {}
struct TupleType(i32, i32);
fn func_2(f: fn(i32, i32) -> TupleType) {}
fn main() {
func_1(EnumExample::Tuple);
func_2(TupleType);
}
Fun context:
👌
as an alias for writing Ok(())
has been (somewhat jokingly) proposed before, though I can’t find the link.
Any solution that allows just writing Ok
doesn’t seem like enough of an improvement to warrant the complexity it adds, even if it weren’t ambiguous to the constructor fn.
One more whacky idea on theme, then I bromise I’ll stop.
What about a FromNothing or ReturnNothing trait.
Implemented by () and Result<Ok,_>.
When the signature returns a FromNothing type then no need to write a return at the end. And in the middle:
return;
Would be fine.
The worst damage a bad coder could do is implement it for something random type. it might lose a few copiler errors, but I doubt most people would stumble across it in day to day coding as returning something would always overrule FromNothing.
That said. If someone did apply it badly. It might stop some of those useful. “consider removing a semicolon.” errors showing up
Contrived-but-working example for both:
fn foo<R>(f: impl Fn(i32) -> R) -> R {
f(0)
}
fn bar() -> impl Fn(i32) -> Result<i32, ()> {
return Ok;
}
fn main() {
assert_eq!(foo(Ok), bar()(0));
assert_eq!(Some(1).map(Ok), Some(bar()(1)));
}
Agreed. I want to be able to think in a success-path mindset while writing error-aware code using ?
the same way I will be able to think in a synchronous-path mindset while writing concurrency-aware code using await
.
Yeah, I guess that’s why the empty result is such an anomaly, the happy path doesn’t return anything. Only the unhappy paths.
Which is the main reason this has come up a few times before lol
Having to write a return for a non return feels odd. Though I do understand its current necessity.
It's not a "non-return" though. (And you also don't have to write an explicit return
, but that's a different question). Unfortunately, many people these days confuse "no return" (i.e. divergence) and "return with data of no information contents". I guess this is a result of decades of bad pedagogy: mainstream languages such as C and Java have been calling the unit type void
since forever, and many didn't even have the distinction between a function that doesn't ever return and one that returns no information.
The first class of functions, those which diverge (e.g. process::exit()
, or something that loops forever, e.g. a HTTP server) would/could have a return type of !
, also called never
in Rust (and also, correctly, called void
in some functional languages). In contrast, a function that does return but with no information returns ()
(the empty tuple, usually called "unit" in the functional world), and there's a special case in Rust whereby omitting an explicit return type from an fn
item results in an assumed return type of ()
, and omitting a return value will imply ()
(the value) to be returned too. Again, this is a special case and not something natural or regular.
Note that there are two, distinct levels of "nothingness" going on here. !
is a type that doesn't have any possible values (it's uninhabited), whereas ()
is a type that does have a possible value (ie. it's inhabited), but its information content is zero because there's only one possible value for it.
Technically this is not a special case, just the normal "final expression is return value" rule combined with ;
terminated statements having the value ()
. You can see this with this snippet
fn foo() {
5
}
giving a type mismatch error
error[E0308]: mismatched types
--> src/lib.rs:2:5
|
1 | fn foo() {
| - possibly return type missing here?
2 | 5
| ^ expected (), found integral variable
|
= note: expected type `()`
found type `{integer}`
However, fn foo() {}
still compiles even though there’s no final (or any) expression or statement inside the body. (However, this is consistent with other blocks’ semantics – and I’m definitely not arguing against this behavior.)
Interesting, so it’s more "omitting any expression in a block will give the block a value of ()
", which combined with the implicit return rule gives the expected behaviour. This even extends to try
blocks which is nice to see
#![feature(try_blocks)]
fn foo() -> Result<(), ()> {
try {}
}
And thats what makes the idea of a FromNothing trait seem workable.
Just want to inject that in my opinion Ok(())
is certainly returning a value (a success flag). Returning nothing would be plain ()
, which you don’t have to write.
A Result<(), ()>
is like a semantically loaded success-boolean. To me, the most important part of any Result
return value is the Ok
or Err
. As such, I get much value out of it being written down in code.
the happy path doesn’t return anything. Only the unhappy paths.
There's no such thing as happy or unhappy paths in general; that's an application-specific qualification. It's as if you're proposing to eliminate Ok
to "make the database path easier to write." And I'm looking at my programs wondering where the database path is...
Result is a general mechanism, and Ok / Err are symmetrical. It is certainly possible to write programs where Err
is an expected and normal return value, and even the most common one. If your application makes a distinction between happy/unhappy paths, that's something you have to program within your application, because that is certainly not something that every application does. It's a specific architectural decision that you're making.
I’d say Err being the happier response is a rare case. I see matching various error types regularly enough but “?” Exists specifically for the happy path style pattern, so it’s common enough to merit.
Using the FromNothing trait idea would not stop anyone from specifically writing OK(()) where they felt it fit.
Without a corresponding lint though, the absence of Ok
would provide less information then.
It’d still be in the signature.