Just a pondering.on result<(),T>

If a function returns “()” you don’t need to write “()” at the end of the function.

Could we do that with “Ok(())” if the function only throws errors to a Result<(),T>

1 Like

This is a feature that’s received extensive consideration in the past, and its very controversial. There are a lot of variations of it, you can search terms like “Ok wrapping,” “catching functions,” or “throwing functions” to find some the discussion.

8 Likes

As someone who doesn’t want such a thing, my argument would be that Result is not really a part of the language per se, but that it is a library type, and special casing the language for it is problematic for a number of reasons: The rule right now is to return an enum variant, you name the variant; Result stops being ‘just an enum’ when it has special language support that user types don’t have; why not Err(()) and Some(()), et al; Ok(()) already communicates perfectly clearly what is happening, etc.

9 Likes

Only the Try trait will have language level support. You will be able to write the following functions:

try fn foo1() -> Result<(), i32> {
    if flag1 { fail 10; } // equivalent to `return Err(10);`
    if flag2 { pass; } // equivalent to `return Ok(());`
    if flag3 { return Ok(()); } // you can still use explicit returns
    do_something()?; // and `?` works as expected
    // `try fn` implements wrapping, so no need for `Ok(())`
}

try fn foo2() -> Result<u32, Error> {
    do_stuff1()?;
    // error type will be converted here if needed, no need for `map_err`
    do_stuff2()?
}

try fn foo3() -> Result<i32, ()> {
    if flag1 { fail; } // equivalent to `return Err(());`
    if flag2 { pass 10; }
    20
}

// same but for `Option`
try fn bar() -> Option<u32> {
    if flag1 { fail; } // equivalent to `return None;`
    if flag2 { pass 10; } // equivalent to `return Some(10);`
    do_something()?;
    20 // equivalent to `Some(20)`
}


try fn barz) -> Option<()> {
    if flag1 { fail; }
    if flag2 { pass; }
    do_something()?;
    // no need for `Some(())` at the end
}

In other words try fns will not be exclusive for Result types, but we will be able to use them with Option and even with user-defined types which will implement Try trait.

1 Like

I thought the Try trait was something independent from the proposed try keyword on functions, and auto-wrapping. Is there an accepted RFC for the try keyword?

It has started from catch blocks in RFC 243, which then evolved to try blocks (see RFC 2388). After that there was @withoutboats’s pre-RFC and in the discussion several evolved variants were proposed, one of which I’ve presented in the previous message. try fn is a logical evolution considering async fn, desire for try { .. } blocks and commonness of error handling, which ideally should be more ergonomic to read and write.

Unless stuff has changed since I last looked at this, it is currently

if flag1 { fail NoneError; }

That sounds bad (i.e. coercion-like). Ok-wrapping ought to be unconditional- the body looks like it returns a T (in this case ()), and it gets wrapped in Ok, period.

I'd also leave out pass/fail entirely. async doesn't expose yield, only await!, so try shouldn't expose fail, only ?. The actual "introduction of the effect" so to speak should just be a library function---for async, some utility Future impl; for try, either Err or a wrapper fn fail<T, E>(e: E) -> Result<T, E> { Err(e) } for those who don't like Err(e)?.

As I’ve said this is just one of the variants. The main goal of the post was to demonstrate that @skysch’s post was not correct.

I am fine with return wrapping as well, though I don’t think that using fail(e)? makes any sense compared to Err(e)?. I think that we need fail keyword to make it explicit that this statement returns an error as-is, without any transformations, while ? is conditional, “unwrap Ok, or return a potentially converted error”.

As for pass and wrapping return, some voiced concerns about adding wrapping behaviour to return (though one can say that it will be incoherent to wrap last statement, but not explicit return), which could be confusing. Meanwhile fail/pass dichotomy is quite clear and easy to learn, and the cost of additional keywords will be somewhat alleviated by the fact that they can only be used in try contexts. (i.e. you will be able to use pass variable outside of such contexts, though I am not sure if others will like such “contextual keywords”)

It is not exactly clear... people are going to be writing code with abort, panic, fail, Error, Err in them. And the only of those that definitively represent something going wrong is panic and abort. People are already apparently having trouble learning error handling in rust, and I'm skeptical introducing a fail keyword and implicitly creating enum values improves the situation for anybody except those who already know how it all works. (Even if there is a good distinction between pass/fail, you also have to understand how they differ from Ok/Err. Beginner-friendly indeed!)

People are regularly bringing up terms like "exceptional conditions" and "unhappy paths" in the context of Result, even though (AFAIK) there isn't any mechanism for controlling branch prediction and code locality within result... which is burying the actual relevant mechanisms for building good code behind so many layers of pop-jargon that it's almost impossible to tell what anybody actually wants their code to do other than write itself.

3 Likes

the biggest argument against the original suggestion makes a lot of sense to me. “Result is not part of the language”.

it’s why I nearly didn’t start this thread. I’m not surprised to hear it’s been talked about before or that some other options like “pass/fail” might crop up.

for my two cents. I love the rust Result and.Option types especially when used with the “?” operator and from types.

The “?” works well as it depends on the From/Into traits. something that is unambiguous and very powerful.

This might seem really off, but I’m curious about what people will think about this.

What about if return coercion was allowed using “From/Into” like the “?” this would be unambiguous as the return type is part of the signature.

That way () can coerce to Ok(()) by impementing

impl From<()> for Result<(),_> {...}

This might be a bit overkill, but I can’t see it being abused badly as it would still be easy enough to read even when someone has tried something weird.

It would be easy enough to figure out (not read) if you only write correct code. If you write something wrong, the From impl might convert your error into successfully compiling code, and then you’ve got to debug that. It also hides a function call at the end of every function, which doesn’t seem desirable for two reasons: first, it may not always perform well – and in that case, it wants to be opt-in… but it already is (just call into) – second, it will be surprising to see functions behaving oddly all over the place:

fn foo(a: u32) -> u64 {
    if a > 3 { a } else { 2u8 }
}

Which would starkly contrast how things work everywhere else:

let x: u64 = 5u32; // Why is this an error but not the above??
3 Likes

yeah that makes sense. I can see a few Froms being writtten in error and causing some confusion.

I guess sometimes it’s just better to be explicit. Lol

Here’s a possibility I don’t think I’ve seen in previous iterations of this discussion. What if we allowed

fn foo() -> Result<(), Error> {
    do_stuff1()?;
    do_stuff2()?;
    Ok
}

whenever it is currently valid to write

fn foo() -> Result<(), Error> {
    do_stuff1()?;
    do_stuff2()?;
    Ok(())
}

? I know I for one don’t mind writing the explicit Ok but find the (()) to be annoying visual noise and also difficult to type.

(Implementation handwave: for any enum constructor taking one argument, allow bareword invocation of that constructor as shorthand for passing () as its argument.)

The problem is that this is fallback logic, and fallback logic mixes poorly with inference -- see all the problems with coercion and {integer} in generic situations. foo(Ok) and return Ok are already legal code, since Ok is impl Fn(T) -> Result<T,E>.

A related but I think simpler handwave: Allow Ok() by automatically adding trailing () arguments if you don't pass enough to a function. (Though I'd still rather try fn to that.)

1 Like

Maybe it's because I haven't done sufficiently complicated things with types, or maybe it's because I'm getting over a cold and not thinking all that clearly, but I don't understand what either foo(Ok) or return Ok actually means now. Could you give more complete examples, please?

Or just have a constant in stdlib

const OK = Ok(());

That way it’s not even a language change. This is really the only significant use case for it.

I’ll bet it’s a breaking change though as someone somewhere will have used OK as a const somewhere.

That said scoping might mean it’s not a problem.

– Actually as this will need to type correctly we might be dependent on genericish constants.

1 Like

Result::Ok is already a function, and you can return functions in Rust.

How is it distinguished from the enum then?

The enum is Result, while Result::Ok and Result::Err are functions which construct a Result.

1 Like