Auto wrap of () in Ok() for functions returning Result<(), Error_Type>

I find myself quite frequently creating alias types, such as

const OK: Result = Ok(());

type Result<T=()> = RR<T, Box<dyn EE>>;

use std::{result::Result as RR, error::Error as EE};

Just to be able to end a function with a succinct - OK - rather than weirdly looking Ok(()).

If the compiler is able to intelligently transform

async fn execute() -> Type

into a much more complex

fn execute() -> Future<Output=Type>

under the hood, why not remove the pain of Ok(()) for the Result's as well? After all, it doesn't force you to write to return the Unit type for each function without an explicit type to return. Why should anyone be forced to repeat himself over and over just to wrap an obvious lack of errors in an Ok(())?

1 Like

This is closely related to try blocks, which have the auto-wrapping behavior you desire.

There have been a few different proposals for try fn or fn = try syntax that performs this wrapping at the function level, analogous to async fn.

The fehler crate implements a similar syntax as a macro.

Another somewhat relevant discussion recently: [Pre-RFC] `Result::void()` and `Option::void()`

5 Likes

It's interesting that it actually does do that, it's just the unit spelled as ;. Ie, we can imagine a hypothetical Rust which doesn't have ;, and just infers whether you meant to return a value or a unit from the function. In today's Rust, ; is a syntactic marker which distinguishes unit blocks from value blocks.

4 Likes

The author of fehler also wrote a blog post arguing for automatic Ok wrapping: A brief apology of Ok-Wrapping.

2 Likes

Although the idea has its place, it's a bit too cumbersome - the "try" syntax doesn't belong in Rust, although that might just be might own opinionated mind speaking. It does have its use, namely:

let result: Result<i32, ParseIntError> = try {
    "1".parse::<i32>()?
        + "2".parse::<i32>()?
        + "3".parse::<i32>()?
};

however, it's a bit different from a silent wrap in an Ok at the end. This is explicit, a wrap isn't - which is where its beauty lies and why we're able to omit the Units at the end of the functions' body.

That's actually very neat - I might just use it the next time. A silent wrap would still be better.

Yup, exactly my point. I especially relate to this:

I understand the logic behind the Unit, but the parenthesis were never meant to wrap an empty space. If we know there's nothing to return, and we know we need to return a Result, let's just do it.

Another reason why I'd love to have a silent Ok wrapper is that it doesn't change anything - no new language constructs, no complexities - just a compiler to check to see if there's a () at the end of a function that should be returning a Result<(), _> - and silently handling it. No complications.

Oh, wow - really? Didn't know that. So each and every time I end a statement I actually return a Unit from it? That's fascinating to know. But I still want my wrappers.

By the way, there's no need to introduce any compiler checks, as such. A simple automatic conversion using a From trait would suffice. Which is impossible to implement by hand:

impl<E> From<()> for std::result::Result<(), E> {
    fn from(_: ()) -> Self {
        Ok(())
    }
}

Due to this:

only traits defined in the current crate can be implemented for arbitrary types
define and implement a trait or new type insteadrustcE0117
main.rs(19, 22): `std::result::Result` is not defined in the current crate
main.rs(19, 9): this is not defined in the current crate because tuples are always foreign

So there's nothing stopping it from being implemented inside the library itself.

This has been discussed before. I think it's a bad idea to special-case a type into the core language just because you are annoyed with typing Ok(()). Small syntactic "conveniences" (I don't really buy that it is at all convenient…) are not a good reason for changing the core language. (And please don't get me started with replying that Result is already special because of the ?‘ operator – that works via the Try` trait.)

Furthermore, I would argue that seeing the Ok part is important. In a Result-returning function, errors can occur, and you absolutely want to handle errors and make sure that the function only succeeds if you unambiguously stated that it should succeed. Accidentally returning Ok when in fact you didn't mean to would have disastrous consequences.

And it is also valuable for 3rd-party readers of the code, for the same reason. They won't need to go hunting for the "happy path", they will see exactly when the function is allowed to succeed.

8 Likes

True, and it's been discussed for a reason. Do you enjoy having to type Ok(()) all over the place whenever your function has to do a lot of error checking? I highly doubt it.

For some pattern matching is nothing but a convenience feature - we could all use the standard "switch" operator the way good old fashioned C does, couldn't we? Yet Rust is loved, in part, thanks to the features, some of which were added purely for convenience. The "if let"s , the "Try"s are not there because it's impossible to work without them - they are there because it makes sense.

Your argument is the same as the one tackled by the author of fehler:

Summary

The error path should not be invisible

This is the most common counterargument and it is very easy to respond to: I agree that the error path should not be invisible. That’s why I’ve never advocated making any change to how the error path is handled. It is still necessary to use ? to “rethrow” an exception.

And yet it’s always one of the first and most popular comments any time this discussion is brought up again. I wish people would put a bit more effort in understanding things before making comments on them, but all evidence suggests this is not the natural behavior of humans on the internet.

Let me just repeat: no one is proposing to make the error path invisible. If you think that is the consequence of throwing/catching function proposals, you have misunderstood these proposals and you should start again.

I actually find this complaint quite ironic, because ok-wrapping syntax would make it easier to identify all of the error paths because of the way it interacts with implicit final return. Today, when a function call returning a result ends in an implicit return, if that function is also returning a result, it is totally unmarked. But this syntax would require, even in that case, that that terminal function call be annotated with ?, identifying it as fallible. With this syntax, now every fallible path is marked with ? or a throw expression. This is more consistent and more explicit!

To avoid strawman'ing your point, I can say that I do understand where you're coming from - silent errors are the worst. However, whenever you declare an fn with arguments and no return type, which ends with a statement, you're silently dropping an explicit () which might be useful for someone to understand that the function has officially finished its work. But we don't do that - because it's not that important, is it? It takes a special kind of negligence to go through 5 to 10 tries of ? only to let the function silently proceed up to wrapping a non-result () in an Ok only to discover the error later. The same kind of negligence it would take to cause dead-locks with mutexes - but that's bairly a reason to avoid using them, or to retype 4 parenthesis near the Ok over and over.

Seeing an Ok is definitely important - when there's something to see. Any Result is an enum - which either succeeds or it doesn't. And if there's no success value to provide back to the stack, it makes no sense to be repetitively explicit about the fact that the function is done, but there's nothing to return.

Once again, if it would just my own pet peeve, I wouldn't mind - but there are quite a few people for whom it's not only not a fun exercise, but a completely meaningless one at that.

2 Likes

I don't enjoy it; I don't mind it, either. I do enjoy using a language with uniform and consistent semantics without surprising and cryptic special cases, though.

That is categorically false. It is also a pretty bad analogy. Pattern matching is a much larger and much more general feature, and algebraic sum types (enums) basically can't usefully exist without it. Pattern matching is about extracting structure from values, and not merely about bestowing if foo == 0 with a superficially new syntax. The need for pattern matching permeates a language with an algebraic type system, and so its benefits are voluminous and manyfold.

In contrast, your proposed Ok-wrapping feature is nothing but a one-shot convenience, which would not have any other benefits besides its single, very narrow use case, and even the problem it would be solving isn't much of a problem in itself. Typing 6 fewer characters is not a virtue in itself.

8 Likes

Since your main issue is with typing the exact string Ok(()), maybe you could bind it to a function key, or some other easy to type key combination? I've done something similar in the past, and it doesn't require a change to the language to do it, just that your text editor support setting key bindings like this.

1 Like

I explored some form of coercion as they way to do this back in 2017, at the request of the lang team (of which I was not a member at the time) in https://github.com/rust-lang/rust/issues/41414#issuecomment-298096222.

It turned out, however, that doing that without an opt-in has a whole bunch of problems. So it exists only as a never-actually-proposed version of https://github.com/rust-lang/rfcs/pull/2107.

Those reasons don't necessarily apply if it's just for Ok(()), but I'm particularly not a fan of this being that specific. And even then, what's Foo::foo() supposed to do if it's a generic that can be either Err::<(), _>(...) or ()?

Most of all, I'd say it's unlikely that anything will happen in this area until more progress is made towards getting try{} stabilized, as the visible opt-in for the wrapping behaviour.

imo if we ever get fn foo() -> Foo = try { … } (with a generic expression), any argument for Ok(()) elision becomes moot.

3 Likes

That would be my preferred syntax as well (with Foo being something that supports Try, such as Result).

Has there ever been an RFC for that syntax? I have seen it discussed on a few occasions and I don't recall any major objections to it, while at the same time seeing reasonable support.

I'll note that I think I'm somewhat negative on it, because my experience with a similar feature in C# -- where it's more useful because it saves a required return -- has been mixed at best because it's yet another thing to argue about in code review.

Looking at

fn foo() -> T = try { x };

It's saving a grand total of one space compared to

fn foo() -> T { try { x } }

(And it opens the question of whether anything can go there, or only block constructs, or ...)

In comparison, the C# version of

public int Foo => 4;

instead of

public int Foo { get { return 4; } }

is a much more meaningful difference.

(Now, of course someone will say that there's an indentation level difference. And that's true, but it doesn't really sway me.)

I don't see much point in introducing fn foo() -> T = try { x } syntax. Not only, as argued in the previous comment, it does not save much space, but we also have the prior art of async fns. try fns would not only be coherent with them, but also IMO quite easier to read than the = syntax. Both async and try can be viewed as a function "property", together with unsafe and pub. In future it may be nice to have other "properties" as well, such as nopanic and coroutine.

1 Like

The big difference between async and try in this capacity is that async {} always returns an opaque impl Future, whereas try {} returns a concrete impl Try.

This means that while async fn can comfortably just put the "inner" type in the signature, try needs to specify the "outer" type. And even if you do restrict try functions to Result or even have them return opaque trait implementations, you have two "inner" types to specify, which doesn't fit in a normal signature.

I can buy that fn() -> $ty = $block_expr isn't motivated enough, but it's not because try fn solves it better, because try fn has its own problems which far outweigh the minimal concerns (and small benefit) of fn=.

2 Likes

This difference is irrelevant in the context of looking at try as a function property. pub, unsafe and hypothetical nopanic properties also return concrete types (although not bound to a trait).

What are those problems exactly which are not inherent to the fn= syntax as well? Note that I am proponent of the try fn foo() -> Result<T, E> { .. } flavor, i.e. of try fns which explicitly return a concrete impl Try type.

FWIW, there's currently an ongoing discussion about whether async functions should specify the "outer" type.

1 Like

The reason this came up again, is cause there's this push to forcefully ignore an essential observable property. But hey, people keep noticing the bugs in the matrix and bring this up for discussion again.

Let's quickly recap:
In Haskell, do notation is used to sequence operations. Rust on the other hand (like all other normal languages) simply has the default reversed - operations (separated by semi-colons) are sequenced by default. This means that the ? operator is an unbind action (opposite of Haskell's bind) and that regular Rust code is equivalent to Haskell code within a do-notation block - a so called ambient monad.

So what's missing?
A monad requires two operations bind and return. We are missing the latter. The recent stabilisation of ControlFlow and the Try trait version 2 is actually frustratingly so very close to this realisation already. We just need a dash of sugar on top.

In concrete terms, the from_output method on the Try trait is that missing return operation and all we need is to tie it to the return keyword and the implicit return of the last expression. This would even be backwards compatible by doing something analogous to auto-deref on type mismatch based on the return type.
For example:

fn foo() -> Result<i32, Error>
{ 
     bar()?;
     42 // auto-wraps into OK(42) by calling <Result as Try>::from_output() 
}

Similarly, we should adopt the outer-type proposal for async functions and drop the redundant async from the function signature, making everything consistent and less syntax heavy.

5 Likes