Pre-RFC: Catching Functions


#103

Yes, I think this hits the issue pretty well for me. Syntactically, I’m ok with the similar pattern, but it should be clear that Rust is not semantically like any form of unwinding exceptions.


I think we differ on this. I learned Result before ?. Maybe it’s just confirmation bias, but I feel like that is the more intuitive order to learn. Result is far simpler and more intuitive than ? IMHO. Result leads naturally to ? once someone has written enough code to wonder about ergonomics. Moreover …

How did you come to this conclusion? Personally, that’s not the experience I had. And I don’t think it’s the experience of people I have worked with while they were learning rust. And I don’t even think this is the approach take by The Book. Is it based on a survey or something?


I can appreciate this, but personally, I would be ok with not building out the whole system all at once. This pre-RFC is a bit too ambitious IMHO.

I really don’t want implicit Ok-wrapping. But I am ok (no pun untended :stuck_out_tongue: ) with something like throw provided that it is not conflated with exceptions and doesn’t reinforce that intuition (as per @chriskrycho’s arguments). I tend to think of those as two distinct features that could exist without each other and still be useful, just as ? does today.


#104

What do you mean by implicit? I think everyone involved in the conversation agrees that the fact that ok wrapping is happening must be locally visible in the function implementation. Some people want to go further and have it be syntactically distinct from non-ok wrapping returns.


#105

I don’t understand. Can something be locally visible and not be syntactically different?


#106

I really appreciate @chriskrycho’s perspective, especially on the return value syntax. Keeping -> Result<T, E> is important to keep the complexity down, to give beginners an obvious path forward for learning more, and because this sugar (however it looks in the end) is purely local to the function and thus not the only idiomatic way to generate such a return type.

However, I also think catch blocks are important as a way to localize the effect of ? to an expression. The catch block itself doing Try::from_ok is the perfect complement to ? doing Try::from_error, and makes far more sense to me than either catch { Ok(x) } or any sort of coercion-like Ok-wrapping. In that light, the -> Result<T, E> catch { syntax is a good, conservative, straightforward way to extend the benefits of Try::from_ok to functions as a whole.

I might even go a little further and introduce a more general fn f() -> T = ... syntax to make function-level catch less of a special case. This resolves the catch E syntax question in favor of just sticking to using type annotations (-> Result<T, E> = catch {, let x: Result<T, E> = catch {), extends nicely to other situations (e.g. other function-level wrappers, private-fn type inference), and has precedent in both C# and Kotlin.

I’m less convinced of throw e and return t. throw because it’s too similar to (labeled-)break-with-value to merit an entirely new syntax, and return because it doesn’t have a counterpart for expression-level catch. Further, if we used the fn f() -> T = syntax above, return wouldn’t even make sense as the catch block would be one layer inside the function body itself. I would prefer whatever solution we come up with here both a) align well with (labeled-)break-with-value and b) apply equally to function-level and expression-level catch. Perhaps simply some kind of break catch?


#107

Thanks for the clarification =)

Yep - it is the same proposal. I was going to take it further and introduce 2 new traits Fail (sorry, your https://docs.rs/failure/0.1.1/failure/trait.Fail.html trait will have to be renamed =D) and Wrap or Pass with just the one operation and then Try will extend those 2 traits.

Not true - you can end the function with pass (). To me, annotating the function signature to get OK-wrapping feels too non-local.

The advantages of a trait is that arbitrary code can be executed in its methods, so it is usable for things like Future.

You can get rid of String::from(, so it would just be throw "😱"

I think it is also not just about reducing boilerplate, but rather to make the success and fail paths more clearly annotated.


#108

@withoutboats Can we extract the type ascription thingy on catch out of this RFC? I think it is sufficiently decoupled from the rest to be discussed independently.


#109

I’m trying to draw the distinction between making a marker at the function header and making a marker at each return site.


#110

I see, thanks for explaining.


#111

Just some ideas.

// err prefix means - this is our failure type
// it isn't part of an external signature
fn write_it(&self) -> Result<usize, err usize> { 

	if self.size > TOO_BIG_SIZE {
		fail self.size; // autowrap in Err
	}
	self.write(&self.data)?;
	Ok(self.size)
}
// fail, err - up to debate; could be the same keyword, could be different


// Could be even pushed more into both cases,
// for convenient autowrapping
fn write_it(&self) -> Result<ok usize, err usize> { 

	if self.size > TOO_BIG_SIZE {
		fail self.size; // autowrap in Err
	}
	self.write(&self.data)?;
	ok self.size // autowrap
}

// This seem to work with other `Try`
fn write_it(&self) -> impl Future<Item = i32, err Error = io::Error> { 
	// ...
	fail e;
	// ...
}

// or maybe
fn write_it(&self) -> impl Future<Item = i32,  Error = err io::Error> { 
    // ...
}

I’d also like to add that I find catch and throw confusing. try and fail seem less meaning-charged from other languages with actual exceptions.


#112

Since it’s not been linked yet, here’s my stab at this from last August (Rendered):

I’m by no means wedded to the syntax in there, but I still believe in its rationale. Also, I think I’m more desiring of it working with non-Result types than many in this thread. To reuse an example, I really don’t want to see something like this

fn foo() -> String throws Async<io::Error> as Poll<String, io::Error>

I haven’t seen closures discussed yet here. I assume whatever marker is chosen here should also work for them so I can do, say, |x|? v.get(x)?.to_string().

I would certainly be in favor of the throw sugar. My thought there was that unlike ? it wouldn’t do a From::from conversion on the value, so that inference can flow into it, since one can always do throw e.into() if needed. What were you expecting there?


#113

Along the same lines, I think its a serious mistake to substitute “easy to explain how it works” for “easy to understand how to use it.”

That’s certainly a good point, the question is if this proposal makes the understanding even harder.

Lets say that this propsal makes it easier for newcomers to write their first functions with error handling, because they could somehow use their knowledge of exceptions in other languages.

Now they have build some kind of intuition for the error handling in Rust, but if they go deeper they will see that their first intuition was completely wrong.

So there will be some kind of bump in the learning, with this syntactic sugar or without it, but this propsal feels more like adding another artificial bump, without removing/reducing the bump that’s already there.


#114

How would you feel about generalising this idea to auto-wrapping blocks?

let x: Result<i32, Error> = wrap {
    if bar() {
        break 0;
    } else if baz() {
        break MyError;
    }

    quux()?;

    7
}

Wrapping a function:

fn foo() -> Result<i32, Error> = wrap {
    if bar() {
        break 0;
    } else if baz() {
        break MyError;
    }

    quux()?;

    7
}

These blocks would of course only be applicable to enums where the variants are distinct in terms of the values being wrapped. Option<T> can be supported unambiguously by using () for None.


#115

Probably this is the deeper point of disagreement. I think their intuition will be completely right. The intuitions I’m talking about have less to do with literal runtime representation than with a conception of use. I think the intuition users will gain from this is:

  • In functions with -> T catch E, you can throw errors with ? or throw.
  • You can return the happy path case with return
  • Functions with that signature unify these two paths into a type called Result, which can be rethrown with ? or manipulated like a value.

This maps perfectly to the understanding they need to use these tools effectively.

What they will not gain immediately is an understanding that throw, ? and catch all desugar into something you could build from match, return, Ok and Err.

I think if you ask a new user, today, how ? works, they are likely to have no idea. This is by a wide margin the most complex desugaring of any of these syntaxes, since it both branches and early returns. But they can still use it correctly, despite not knowing what it desugars to - just as they can use for loops correctly without knowing their desugaring.


#116

This maps perfectly to the understanding they need to use these tools effectively.

Newcomers most likely will also look at foreign code to learn from it. If they see there a ‘Result’ return type, and even if they’re able to easily unify ‘T catch E’ with ‘Result<T, E>’, then the error handling in the implementations will look quite a bit different.

So I have a hard time to see how this syntactic sugar should make an effective usage of the language in any way faster.

It might even hinder the learning of enums, because ‘Result’ might be one of the first a newcomer sees and they might get the idea that ’catch’ just works with any kind of enum.

I think if ‘catch’ unifies to ‘Result’, then it’s IMHO also the most intuitive that this unification is directly visible in the syntax. So instead of ‘T catch E’ having something like ‘Result<T, E> catch’.

I somehow like the ideas of @dpc in this thread, having ‘ok 3’ as sugar for ‘Ok(3)’ or even ‘ok’ for ‘Ok(())’, and ‘err “argh;”’ for ‘return Err(“argh”.into());’.

I would call it local sugar, there’s no need for more context information to understand it, you just have to understand the sugar at hand.

‘catch’ feels more like non-local sugar with the need for more context information to understand the code inside of it.


#117

Hi, I believe I still qualify for a “new user”. From my experience the most complex thing about ? is the usage of std::convert::From. In fact I only learnt about it 1-2 days ago because of this thread. Until then for 1/2 year I was able to skim fine through Rust examples without this knowledge

? being an early exit from a function? Result<T, E> return type? Easy-peasy. Piece of cake. No barrier. No brainer. No problem.

Please no. I feel that I absolutely needed to know what ? desugars to. From my first Rust example. From my first line of code. I really did. Well maybe not From::from part. But when there’s no conversion of error type I absolutely wanted and needed to know what the desugaring is.

Again, please not this syntax: fn f() -> T catch E. That’s flat out unreadable. fn f() -> Result<T, E> catch { while still looking weird is way better.

It does feel like this plan improves ergonomics once you know what is going on. In fact this is probably why so many seasoned Rust users support it. On the other hand it arguably makes learning harder. Which is probably why lots of new users don’t find themselves 100% behind the plan.

P.S. if you really wanted to make it Java-like you’d probably make the signature fn f() -> T throws E However because of the lost didactic value I’m not suggesting that one at all. Again if you wanted to make processing errors Java-like you’d make it try{...}catch{...}.

P.P.S. From::from is probably going to be the most complex thing about catch blocks too. That and its uncanny resemblance of C++/Java. So maybe calling it wrap as suggested by @repax would have been a better option. And then it’d be really nice to be able to somehow “return” from do catch or rather wrap blocks inside a function too (as suggested by @rpjohnst and @repax)… perhaps indeed via break value as suggested by @repax. Or using @Centril’s suggested new keywords to also dispose of C++ baggage associated with throw could it become:

fn foo() -> Result<i32, Error> = wrap {
    if bar() {
        pass 0;
    } else if baz() {
        fail MyError;
    }

    quux()?;

    7
}

#118

What are the main reservations against allowing implicit Try::from_ok(From::from(expr)) wrapping for return expr for types that are Try?

In my opinion if we’re going to change the semantics for return for Try types - don’t mark anything at the function signature… just auto-wrap. Otherwise, just add pass as a keyword that does this. The type checker will now just check if the return type T is Try and then, if it can’t unify the return expr with T, it will attempt to unify it with <T as Try>::Ok. If the second unification succeeds, then it will wrap with <T as Try>::from_ok(From::from(expr)). If the second unification fails, it will throw an error. Perhaps this has some caveats wrt. type inference that I haven’t thought about… if you have such concerns, please note them.

To me, this is intuitive and simple.

To this, we can then also add fail <expr>. And consider pass at a later stage for those who want to be more explicit about their intent.

PS: I think the keyword should not be throw but rather fail, as it removes associations with Java-style exceptions.


#119

For the edit distance specifically, I think we should also consider an alternative, namely letting IDEs handle it. To show what I mean, take how IntelliJ IDEA works with Java; if you’re editing a non-throwing function, and you now want to call a function that can throw an exception inside it, IDEA has an easy shortcut for either inserting try {} catch {} blocks automatically or adding throws SomeException to the function signature. You can see it in action here or read a bit more here.

In Rust, the example could be something like having this function:

fn get_one() -> u32 {
    1
}

which you then want to make fallible, by calling some fallible function:

fn get_one() -> u32 {
    is_one_available()?;
    1
}

At this point, IDEA would detect that you have a type mismatch with your return type, letting you use the same shortcut as for Java to either update the return type of your function (with the given error type, if it can be deduced) or to insert a match statement to let you sort it out yourself (Note: I haven’t actually tested IDEA to see if the Rust plugin already does this; it might).

(EDIT: It could also do Ok-wrapping, and probably other things as well.)

I do realize that not everyone uses IDEs, but I fully agree with the aversion to add more complexity to the language if it’s not really needed. Maybe leaving this up to our tools is good enough for everyone?


#120

One of the interesting things about a catch syntax is that we might even be able to do body transforms on the function for “unwind ABIs” for ffi similar to how async/await works with generators, for instance imagine:

#[catch::js]
fn calls_js(a: i32, b: i32) -> Result<i32, JsError> catch {
     let c = js_function(a, b)?;
     let d = js_function2(b, c)?;
     if d < 10 {
          fail JsError::new("d is less than 10!");
     }
     d
}

De-sugars to something like:

fn calls_js(a: i32, b: i32) -> Result<i32, JsError> {
    js!{
        try {
            const c = js_function(a, b);
            const d = js_function(c, d);
            if d < 10 {
                throw new Error("d is less than 10!");
            }
            @{return d}
        } catch (e) {
           @{return e.into()}; 
        }
    }
}

#121

I very much favor this idea. And, in fact, I like fail far better than throw, precisely because it doesn’t look like exception handling.

Regarding the keyword for returning successfully: pass has the problem of looking like Python’s “do nothing” statement, and I’d like to not massively confuse the mental parsers of Python programmers. I do appreciate the symmetry with fail though.

But I don’t want to bikeshed here, and any solution that uses a new keyword instead of return seems completely reasonable to me.


#122

Is wrap instead of pass acceptable to you? How about succ? (it may perhaps confuse people with successor as in the successor of a natural number)

I agree that it may confuse some python users, as I noted before.