Pre-RFC: Catching Functions


#8

It’s certainly even worse if return sometimes wraps and sometimes doesn’t within the same function. But even if not, having return sometimes wrap at all, even if it consistently does so within the same function, still feels wrong. Again, you’re not returning 0, you’re returning Ok(0), and hiding that is not something I want to see when reading other people’s code.

That’s why I suggested that I’d find it much more palatable if it didn’t use return. A separate keyword that always implies that automatic coercion seems fine.


#9

The solution to this is the async/await notation, which takes functions that seem to produce Results and instead produce futures. Your leaf function, with the combination of async and catching:

#[async]
fn leaf(success: bool) -> String catch String {
    if success {
        String::from("yeah")
    } else {
        throw "ouch";
    }
}

(your function doesn’t actually do any async IO, but I guess we are to imagine some sort of await'd future call in there)

Here are some other examples in which the return type of a function is special:

  • Returning ! on stable (generalized to a first class type on nightly).
  • The impl Trait “type” means different things in return and argument position; I can’t recall if its been generalized to be allowed in arbitrary type position (e.g. as the ascription of a let) yet.
  • Omitting return types means returning the () type.
  • Generators return impl Generator, but that’s unlikely to appear in whatever syntax we decide for them.

In other words, its not uncommon for the function signature to not be literally (types) -> type.


#10

@josh Do you feel similarly about generators, which return a state machine that eventually evaluates to their return type, but in every syntax proposal I’ve seen use the return keyword and their return type to signify what the generator’s final evaluation will result in?

Is this supposed to be a cheeky way to say you don’t like this proposal or a request for something I don’t understand? I don’t know what “normalized in the code” means if isn’t just “don’t implement this feature.”


#11

async/await is relying on an explicitly experimental RFC. I’d prefer to not discuss a new, independent syntax in the context of this.

Also, this would then special case Futures in the context of Results. That building will fall over at some point.

I took futures as an example for a classic type that has a relationship to Result, but is not result. Any other type would have similar problems.

Yes, it’s just a mock.

You’re right, I wasn’t clear on that. I’m still not sure I’d like to see another addition to that list.


#12

One of the chief ergonomic advantages I failed to layout in the original post was that this proposal is designed to have a very small edit distance when introducing fallibility to a function.

In my experience, this isn’t an uncommon experience. As an example, some unit of code which previously was pure now needs to do some IO (say because its pulling a value from some sort of shared cache that it used to compute). Maybe this isn’t the best design and all IO should be segregated from logic etc etc, but in real systems these sorts of things happen, whatever.

Today, there is a significant amount of purely boilerplate edits you have to make to enable this. In addition to the actual material changes you’re making, you have to change the return type of the function and find every place it returns and wrap them all in Ok. When I have the misfortune of making these edits in a function that ends with a match statement, I’m often tempted to use a regex - to mixed results usually.

But all of this could be eliminated - you add the code you want, you add catch ErrorType to the function signature, you handle the error everywhere it is called (probably by ?). In other words, we reduce the edit distance to the material changes being made to the code.


The interaction with async/await is one of the key insights that makes this proposal work well. Though the async/await RFC was experimental, it’s extremely well motivated and its hard to imagine a world in which we don’t someday have a form of async function which appears to return a result and actually returns a future.

Which types? Result and Future are important to support because they are pervasively important in real code. The weakly supported one here is Option, its true. I’m not convinced that “some wrapping” is so important though, for these reasons:

  • Its not that common for a function that unconditionally returned T to switch to returning Option<T>, not in the same way that becoming fallible is common.
  • When I write functions that return Option<T>, in my experience the terminal expression is usually None, whereas in result functions the terminal expression is usually Ok(result). In other words, the “happy path” is less predominant in most functions I write that return an Option.

Do you have other pervasively used types that you think an ok-wrapping feature needs to be able to support to be worthwhile? Why is it important to support these types?


#13

I haven’t had time to really digest this proposal yet, but I want to put my hand up for being strongly in favour of some ergonomic improvement here. Error handling is really important, it is a fundamental area which distinguishes high quality code from low quality code. However, like unit tests and documentation, it is an area of programming which feels like taking medicine - we know we should, we know it is good, but it is not fun. Therefore, we should strive to make proper error handling in Rust as ergonomic as possible. Furthermore, although our current system has many advantages, it still feels a bit clunky to use (and especially to convert non-error handling functions into properly error-handling functions) compared to languages like Java with a built-in exception mechanism.

To put it another way, we need a really nice carrot here to bribe people away from unwrap.


Pre-RFC: flexible `try fn`
#14

As I wrote above, this isn’t my experience. This is a fundamental change to a function, there is even a school of programming (which I’m comfortable with) that says such a function should be completely written anew.

I’m also very okay with boilerplate and frankly have the opinion that boilerplate is overrated. Most large systems in this world are written in very boiler-platy languages (e.g. Java, C++), and while this is certainly not something to strive for, it is something that can definitely be accepted.

It still makes for a bad argument. I’m not against seeing features in context of other potential feature and weighting their impact, but opening up a discussion on an eRFC as if it were done is not good support.

Other types include anything that moves towards a result, such as alternative implementations of Future-like concepts or any kind of operation. Indeed, my experience in many complex situations is that moving away from Result towards something more rich is useful.

My argument is that the pervasiveness of Result is a bit of a red herring in my opinion and that symmetric usage of the same syntax and approaches in all contexts has value.


#15

I like auto-wrapping and throw very much. The Err(e)? and Ok(()) are Rust’s syntactical quirks I won’t miss.

The “auto wrapping” concept has to make an implicit “leap” from unwrapped value to Result somewhere, and I think this proposal puts the leap in the right place.

Locally, in the function declaration site, this syntax is entirely consistent: Given a function signature is foo() -> i32 catch Error it looks as if it returns i32 and “catches Error” without a Result in sight, so accepting bare return 0; throw err within the function IMHO is absolutely fine, and consistent with foo() -> i32 declarations which also support return 0 (c.f. how weird would it be if the function type didn’t mention Result, but function body was sometimes required to use Result constructors and sometimes not).

There is a bit of magic if you notice that “from the inside” the function returns i32, but on the outside it returns Result<i32>, but it’s at the API boundary, so if it has to be somewhere, that’s the right place. If Rustdoc documented i32 catch Error functions as Result<i32, Error> then the return type would not be surprising to users of the function.

So it’s well-placed syntax sugar IMHO; +1


#16

I like this set of proposals better than Ok wrapping, which I never had any fundamental philosophical objections to, but which felt a bit meh. These proposals together give a more coherent picture (e.g., the function-level catch can be seen as wrapping the function body in a big catch block).

However, it’s not a slam dunk for me yet, because it doesn’t really work out Option as @withoutboats themselves said. Result needs the sugar more badly, but if this syntax is deliberately leaning on ? and catch, it feels like using it with Option should be at least sensible if your function is structured like a Result-returning one (early returns of None and “regular” returns of Some). With the proposal as-is, it feels like trying to fit a square peg in a round hole:

  • What, if anything, do you write in throw to return None? Existing code can do None?; which really isn’t great compared to return None; — can throw improve this too without having to deal with NoneError? (Does it even have to be improved?)
  • Having to write -> T catch NoneError as Option<T> in the signature is twice as verbose and much uglier than -> Option<T>, even when you then manually wrap the function body in a normal catch.

Unlike @skade, however, I don’t see the need to cover other monadic types like Future with this sugar:

  • Those types are sufficiently different that we should not expect a single solution that works for them as well as for “native control flow” handling Results and Options (short of general do notation, which doesn’t work well in Rust for a variety of reasons).
  • This is already reflected in how ? does not work for futures etc. and they instead get their own separate sugar async/await. While it’s true that async/await are experimental, that they even exist shows that we can’t expect the same sugar to apply to them.

But if there’s a way to support at least Option better (without unduly impacting Result), that would strengthen the analogy with non-function catch blocks.


#17

OK, that helps. I understand your point-of-view better now.

That said, I don’t quite understand why it seems so important that you see the Ok. What makes this particular coercion different in your mind from other coercions we already do on return? (e.g., deref coercions)


#18

So one thought I had when chatting with @withoutboats over IRC is that, if we want to support types beyond Result, we might change from catches E to catches R (where R is the raw return type).

So you might write something like:

fn foo() -> catches io::Result<()> {
    let x = read(input)?;
    let y = write(output)?;
}

Now if you introduce a catch block manually, it looks very similar:

let input = open_some_file();
let output = open_some_file();
let r = catches io::Result<()> {
    let x = read(input)?;
    let y = write(output)?;
};

Maybe here the catches keyword is not the best, though, and of course there’s the obvious downside that I must write the Result part. @withoutboats pointed out that there was a similar proposal at some point to do -> io::Result<()>? as the return type of the function. I like that less, but it’s in a similar vein.


#19

But I see this as also a benefit: after all, now the “external signature” is also clearly visible. That is, it seems a bit like declaring parameters as mut:

fn foo(mut x: T) { ... }

While the mut x appears in the fn header, it is not part of the external signature that concerns our clients. Same holds for the word catches.


#20

I think this really exacerbates @josh’s concern though. As @kornel points out, inside the function it returns T and the signature is -> T. I don’t know how to square this; I still think the type after -> being what return takes is a major advantage, but I would like to figure out a clean way to support Option.


#21

Yes, it creates an inconsistency in that respect. I have been wrestling with this for a bit; I am not sure whether that bothers me or not.

Let me give you some related precent. Consider async functions in C#:

async Task<int> AccessTheWebAsync()
{ 
    ...
    return urlContents.Length;
}

Note that this is not async int AccessTheWebAsync() – note also that it returns an integer.

I’d be interested in looking around for other such precedent. I could see an argument that it’s actually easier to understand this way – that is, that the consistency between the type of argument given to return and the declared return type is not that important, as compared to being able to easily see the “real” signature.


#22

Yeah, as you pointed out in a different context earlier, we already have coercions in returns (deref coercions, but also more radical ones like reference -> pointer). So one could view a catch function to have additional coercions enabled for returned expressions.

Edit: No that doesn’t work. Coercions are optional, while Ok-wrapping should not be.


#23

What strikes me about this proposal is that the catch syntax is very close to something like an anonymous sum type. That is, this:

is very syntactically close to writing

fn foo() -> usize | io::Error { ... }

I realize that catch is semantically a bit more than that, because errors are coerced to a single value, and the type is specifically a Result with all the advantages that has, instead of being anonymous (whatever that might mean if such a thing were to appear in Rust at some point).

But I think anonymous sum types are desirable, so maybe it’s worth considering if catching is really a specialization of that? Here we want to return a binary sum type, with two keywords; one for returning to either of the two branches, with the major difference being that we want one of them (throw) to coerce to a specific type, while the other (return) must give its values as-is.

The big question seems to be how such an anonymous enum might be used as a result in an easy way…


#24

I think I like the overall proposal here. Now, I don’t have a lot of Rust code under my belt, so I can’t really speak to how much it’s actually needed, but I can imagine that it is. Aside from my previous comment, my only real gripe is the naming; and I know this is bikeshed territory, but I think names like catch and throw carry enough baggage to actually put the RFC in a very poor starting position. I’ve certainly winced whenever someone said that work was underway on catch blocks, what with my Java background.

So if we decide that Result is special enough to warrant this sugar, then I think we should choose names that are very specific about implying failure, instead of throw and catch. This is especially important because looking at the actual english words “throw” and “catch”, they seem to imply that some special control flow is happening (which is why they are an excellent fit for exceptions), when that is definitely not the case in Rust.

maybe use

fn foo() -> i32 fails Error {
   fail MyError;
}

I also think it’s good that the names imply a specific usecase (for a result, in this case), to better make it clear that this is not intended as a general mechanism. Unless of course we do want it to be more general.


#25

Repeating some of my concerns voiced in #rust-lang:

The only bit in this RFC I am not comfortable with is -> T catch E being sugar for Result<T, E> + some Ok-wrapping. I’d prefer if it instead was sugar for -> impl Try<Ok = T, Error = E> and from_ok-wrapping instead.

I think in general, it is better to hold the specific error monad abstract and then .into_result() at the top level of your stack of functions… I also think that this composes better.

Of course, not knowning that the type is Result means that using match directly on the result of a computation is not possible.

With respect to #[async] and “return types not being what they seem”, I don’t like seeing Result which is then converted to -> impl Future<..> this implicit conversion is not clear wrt. intent to me.


#26

I’d prefer if it instead was sugar for -> impl Try<Ok = T, Error = E> and from_ok-wrapping instead.

This was exactly what I was just about to bring up, if you look back at the previous thread on this exact topic (interestingly with the opposite name) this was a point that @glaebhoerl made.

You do slightly lose out because you don’t have all the Result methods available in the caller, but the majority of the time you’re just going to be using ? anyway–and if you do need them you can call into_result(). I think avoiding special-casing language features towards Result is worth that though.


#28

One thing I didn’t see cleared up in your later posts is what appears to be a slight misunderstanding of what the semantics specified in the RFC for catch actually are (unless there’s been an update to the RFC that I wasn’t able to find after a brief search). This part of the third section is slightly independent to the extension of having a “catching function”, for a large part because it does not affect return statements at all.

Taking the example from the OP but using an "inner catch" going by the currently specified semantics you would have to write:

fn foo() -> Result<i32, Error> {
    catch {
        if bar() {
            return Ok(0);
       } else if baz() {
           throw MyError;
       }
       quux()?;
       7
    }
}

Specifically, a return statement ignores the fact it’s inside a catch expression, it’s only the implicit block return value that gets Ok/Try::from_ok wrapped.

It’s only when creating a “catching function” that return is affected, and in that case you are saying that your function returns i32 (and catches/throws Error) so it makes sense to write return 0 and have the function implicitly wrap that into Ok(0)/Try::from_ok(0) when retruning to the caller