Pre-RFC: Throwing Functions

#13

There’s no ambiguity in this case, because Rust doesn’t do SFINAE. "T that may be u32" and u32 are always distinct types in Rust already:

| t | ^ expected u32, found type parameter | = note: expected type u32 found type T

0 Likes

#14

From a PR point of view, I find that spelling Ok(()) in functions which return Result<(),E> is actually a good thing. It reads as: “and if we get here, then everything has worked ok”, which helps reinforce the notion that errors have been thought about and treated, and helps distinguish Rust from the perceived problems of exception-based solutions. I for one, am proud of our glorious Ok(()).

My counter-proposal is to simply define :ok_hand: as an alias for Ok(()).

I also don’t see any of the proposals as a win for teaching Rust. For instance, consider a case where a bunch of Result<T,E> gets stored in a data structure instead of T values, ie where a non-monadic function like map() has been used instead of a monadic one. In this case, I think it is easier to make sense of this (failing) example than what you get with throws.

fn faillible_seventeen(i: u32) -> Result<u32, SomeErrorType> {
    Ok(17)
}

fn main() {
    let v = vec![1,2,3,4];
    let vv = v.map(faillible_seventeen);
    assert_eq!(vv, [17,17,17,17]); // This won't type-check.
    println!(success)
}

With throws, the code looks like the following, and it is harder to grasp the situation with the spurious Ok() around each 17.

fn faillible_seventeen(i: u32) -> u32 throws SomeErrorType {
    17
}

fn main() {
    let v = vec![1,2,3,4];
    let vv = v.map(faillible_seventeen);
    assert_eq!(vv, [17,17,17,17]); // This won't type-check
    println!(success)
}

In particular, the second version gives the false impression that the following is not possible:

fn main() {
    let v = vec![1,2,3,4];
    let vv = v.flat_map(faillible_seventeen); // We keep the successful seventeens
    assert_eq!(vv, [17,17,17,17]); 
    println!(success)
}
15 Likes

#15

An example of the sort of function in which (I believe) Ok(()) is just pure noise:

fn write_multihash(w: &mut io::Write, multihash: &MultiHash) -> io::Result<()> {
    w.write_usize_varint(multihash.code())?;
    w.write_usize_varint(multihash.len())?;
    w.write_all(multihash.digest())?;
    Ok(())
}

I also find this example really bad because there is an alternative way to write it and I’m like 70:30 on which way is better

fn write_multihash(w: &mut io::Write, multihash: &MultiHash) -> io::Result<()> {
    w.write_usize_varint(multihash.code())?;
    w.write_usize_varint(multihash.len())?;
    w.write_all(multihash.digest())
}

I’d really love to be able to write this as if it were a normal return-value-less function and have the Ok(()) implied (the first example minus the last line).

2 Likes

#16

I always have been glad that Rust didn’t have special operators for stuff like this, but instead everything was a convention instead. Similar for constructors.

However, I hugely dislike converting X into Ok(X) or into Ok(Some(X)) without any notice, as there is this popular pattern of fn foo() -> Result<(),()> { bar() }. With the proposed implicit coercion, that would be ambiguous, bar could return either a Result<(),()>, or (). I would really really not like such ambiguities to appear in the language, that seems like a big bug to me. Also, unless you make the coercion work on function bodies only (which is a novelty, and would make the language harder to learn) is this ugliness with that you now have to allow stuff like let x: Option<Option<i32>> = 4;.

Instead I would prefer having some explicit syntax, where we opt in to a conversion, and where the fn foo() -> Result<T,E> { bar() } case above is forbidden (because it expects either T or E but not the Result). Which syntax we end up, whether its fn blah()? -> Option<i32> like @scottmcm proposed, or the throws syntax, I don’t really care, as long as its explicit and the conversion is a must-do one.

16 Likes

#17

I agree completely. The function body should evaluate to T or to Result<T, E>, for both to be allowed would be very confusing.

3 Likes

#18

@est31 Thanks for substantiating the feeling I’ve had that something’s fishy with the "just coerce to Ok" approach, but couldn’t articulate :slight_smile:

I think the idea to use ? instead of throws in the signature would make sense if we left off the "Try type" in the same way as the throws proposal does. Because in that case, given fn blah()? -> i32 { 42 }, the intuition would be that the type of blah()? as an expression is precisely i32, which is kinda neat. But it unfortunately doesn’t seem like it can work, because it doesn’t leave anywhere to specify what the error type is.

Other things:

  • Someone suggested that a drawback of throws is that it would only work with Result and not other Try types, but I don’t think this has to be the case. In principle, the body of a throws function would never mention the variants of the Try type it’s returning, they would only be created through ? for errors and wrapping for ‘successes’. So in theory it could return any Try type we choose; but even if it returns Result specifically, Try itself allows it to be converted to a different one. If I’m not missing something, then fn foo() -> X throws Y could desugar into any of fn foo() -> Result<X, Y>, fn foo() -> impl Try<Ok=X, Err=Y> (in which case the only thing the caller could do with it is ? or .try()), or fn foo<Res: Try<Ok=X, Err=Y>() -> Res (in which case foo() would desugar to use Try::from_ok() and Try::from_err() at its exit points to convert to whichever type the caller requests), and they all seem like workable approaches (though I haven’t worked it out in detail). I’m not sure which one might be best.

  • This is of course related to how catch should work. I’m of the opinion that catch should just always Ok-wrap (or Some-wrap, etc.) its result, as in the original RFC, but I haven’t had time to read the GitHub thread about this.

  • This also seems related to the design work around async/await, where #[async] would implicitly wrap a function’s written return type in a Future in a similar way to how throws would wrap it in a Result or Try.

3 Likes

Pre-RFC: Catching Functions
#19
fn foo() -> X throws Y     ==>     fn foo() -> impl Try<Ok=X, Err=Y>

That is an interesting variant that I didn’t consider at all. I actually quite like it.


I’m also on the wagon that for a given function it should either always wrap its return values (whether an implicit or explicit return) in a Try::Ok variant, or never wrap them. Using the throws syntax and given fn bar() -> Result<(), ()> that would mean either of

fn foo() -> Result<(), ()> { bar() }
fn foo() -> () throws () { bar()? }

would work, but

fn foo() -> () throws () { bar() }

would give something like

error[E0308]: mismatched types
  |
2 | fn foo() -> () throws () { bar() }
  |                            ^^^^^ expected (), found enum `std::result::Result`
  |
  = note: expected type `()`
             found type `std::result::Result<(), ()>`

possibly with some help text more specific to throws functions than the normal help.

One downside mentioned in the OP that I haven’t seen discussed is that it means you can’t use a normal return to return a Try::Err variant from the function. Is there any way to make that more ergonomic than having a macro expanding to Err(...)? ?

3 Likes

#20

Why don’t you just write

fn foo() -> Result<()> { catch {
    stuff_that_can_fail()?;
    stuff_that_cannot();
    // no Ok(()) necessary
}}
1 Like

#21

Right, the RFC proposes this as unconditional operation (you as its author know that already, but linking so that other people can read it if they want).

This comment seems to contain the current position of the lang team, quoting it:

I think the general feeling was that it is important to keep catch { x } and fn main() { x } as equivalent. However, we would like to support "auto-wrapping".

With the risk of repeating myself, note that I disagree with those results, I think that its a very small issue if catch and fn main() are not equivalent, but its a pretty big issue if there is auto-wrapping instead.

0 Likes

#22

There are two reasons I think the former is better – other than the Ok(()), of course:

  1. It maintains the “lets the reader determine at a glance where an exception may or may not be thrown” property from the original RFC motivation.
  2. It continues to work if you change the function to a custom error types in the future, since it does error-conversion that just returning doesn’t.
1 Like

#23

I definitely like the “never explicitly construct” direction, and want a throw to also allow creating new errors (in addition to propagating them with ?).

I’m not sure I like any of the three desugars, though. They’re clever, and work great internally to the method, but it seems like none of them would allow the caller to foo().ok_or_else(bar) (respectively: not on Result, not on Try, and not an inferrable context), and thus would prevent using throws for anything returning Option today.

(The impl Try desugar is fun, though, and makes me think some common methods should move to Try, such as unwrap_or. I guess things like map can’t without ATC, though…)

0 Likes

#24

(I assume you didn’t mean “clever” to apply to the fn foo() -> Result<X, Y> desugar, which is just about the most banal one possible?)

Anyway I thought these three were basically the possible desugarings? Can you think of any others?

Couldn’t you get the same result by just using Result::map() instead?

The “not an inferrable context” thing is definitely awkward though. That feels like an unavoidable drawback of Rust’s type-based method resolution: you can’t both leave the choice of type up to the caller and let them avoid specifying what type they want in some way. (If methods were resolved purely based on name, the method itself would be the “some way”, as the compiler could infer that the requested type is Option from it, instead of trying to go in the reverse direction.)

The least-annoying way to make the type specified seems to be UFCS: Option::ok_or_else(foo(), bar) would work.

0 Likes

#25

I feel like the primary complaint here is having to use Ok on the result. In which case I propose people affected by this define a macro return! which corresponds to return Ok(_). It’s as intuitive as any other solution, IMO, without any auto-promotion/coercion issues.

2 Likes

#26

What if special-case only the implicit conversion from () to Ok(()) or Some(()), not from any other type? It allows to leave ?; at the end of function, but not disturbs things more.

1 Like

#27

IMO, @est31 made a pretty good case that even this conversion is too implicit in this post.

3 Likes

#28

Another option could be to simply allow Ok as a shorthand for Ok(()). That is, X for any enum variant X(y) where y: ().


This is how it would look like in the example above:

use std::path::Path;
use std::io::{self, Read};
use std::fs::File;

fn read_file(path: &Path) -> Result<String, io::Error> {
  let mut buffer = String::new();
  let file = File::open(path)?;
  file.read_to_string(&mut buffer)?;
  Ok(buffer)
}

fn main() -> Result<(), io::Error> {
  let content = read_file(&Path::new("test.txt"))?;
  println!("{}", content);
  Ok
}
3 Likes

#29

I don’t see the problem. Ok(()) is easy to write, and if the compiler tells me its missing it often means I haven’t finished writing that function.

return Err(..); does feel a bit cumbersome to use vs throw ..;, but it’s not a big deal IMO. (I’ve been giving my error classes an err method so I can do return SomeError::err(..);.)

7 Likes

#30

On the topic of returning Ok(value) vs simply value, I find the explicitness of Ok(()) easy to read, easy to write and easy to scan for. The return type is always clear and always matches the declaration. I’m finding it hard to say the same with the motivating example above.

I wouldn’t hate this change, but I’d definitely prefer for convention to remain the way it is now. I’m just not sure if saving those few characters is worth the loss in clarity and additional ambiguity.

Many comments in this thread seem to be in favour of allowing fn a() -> Result<T, E> { value }, and there aren’t many comments holding the opinion that things should stay the same. I wonder how much of that is because people haven’t spoken up, vs because we are all in favour of the change.

16 Likes

#31

I absolutely agree.

0 Likes

#33

I’m sympathetic to wanting the empty statement to work here, and it can. If the empty statement is given an inexpressible type which coerces to either () or Result<(), T> as necessary (preferring the former), I think that satisfies most needs.

0 Likes