Pre-RFC: Catching Functions


#143

There are so many valuable ideas in this thread, however, I would like to add a new idea that no one has mentioned.

Why not add an automatic “wrapping” ability for all enum types? I presume we use box keyword to do this. (box can generate different types as already designed, in different contexts)

It works as below:

enum E { 
  L(i32),
  R(String),
}

let var : E = box 1; // wrapping i32 using E::L(i32)
let var : E = if cond { box 1 } else { box s };

fn foo() -> Result<P, Q>
{
    return box p; // if typeof(p) == P, then it will converted to Ok(p)
}

fn foo() -> Result<N, N>
{
  return box n; //  if typeof(n) == N, compiler can't determine whether to use Ok(n) or Err(n). This is compile error
}

#144

My main problem with the catch R syntax is that I think its harder to spot when its being used. I think T catch E is more visible, and this seems valuable to me.

I’m not particularly concerned about not seeing Result in the return type, any more than I am concerned about not seeing impl Generator in the return type of an async function. Users will learn this once & I don’t see it seriously hindering productivity before learning it.


fn foo<F: Fn(&File) -> bool>(&mut self, f: F) -> Result<i32, io::Error> {
    ...
}


fn foo<F: Fn(&File) -> bool>(&mut self, f: F) -> catch Result<i32, io::Error> {
    ...
}

fn foo<F: Fn(&File) -> bool>(&mut self, f: F) -> i32 catch io::Error {
    ...
}

#145

My biggest complaint with it is that it means return Some(4) and return None do drastically-different things in a function that’s -> Option<Option<i32>>.

See https://github.com/scottmcm/rfcs/blob/ok-wrapping/text/0000-ok-wrapping.md#make-this-a-general-coercion for more.

(And there definitely shouldn’t be a From::from in there, since that keeps things like return 4; from working normally.)

But is that function-level catch or a catch expression? It matters if there’s a return in the closure.

It needs to be a bit magic, though, so code is more similar between fallible and infallible functions.

I think counting characters is absolutely the wrong measurement here. The big advantage I want out of this is that it becomes normal to always type the ? after something you know can fail as the first pass, allowing the first pass at the method to be written just like it was infallible.

Reminds me of this paper…

I think this is partially due to a lack of keyword highlighting. Changing a letter to get the highlighting helps it stand out.

fn foo<F: Fn(&File) -> bool>(&mut self, f: F) -> Result<i32, io::Error> {
fn foo<F: Fn(&File) -> bool>(&mut self, f: F) -> match Result<i32, io::Error> {
fn foo<F: Fn(&File) -> bool>(&mut self, f: F) -> i32 match io::Error {

And one thing I like about having the type there is that it allows aliases, as are common today:

fn foo<F: Fn(&File) -> bool>(&mut self, f: F) -> match io::Result<i32> {
                                                // no `Error` anywhere

(To be super-explicit just in case: I’m definitely not suggesting match as the keyword :grin:)


#146

This seems like the kind of argument that was leveled against ? as a replacement for try!.

Also, whatever the new keyword ends up being would not be a shorthand for return Ok(x); it would use Try, just like ? does.

This is exactly how I and others feel about the function-level catch syntax and hiding away the Result type.


#147

@vorner IIUC, you’re trying to say that shorter edit-distance maybe a liability? So if I wanted to start using error handling for a function, it shouldn’t be possible to partially make those edits and still compile. Is that what you are getting at?

I guess that’s part of it. Not the whole (the above already had error handling, just tried to rewrite it to the „new“ system), but yes, Rust is generally good at being clear about your intention (that Err(Base) being success isn’t) and about „when it compiles, it’s almost certainly correct“.

It may be worth revisiting the idea of try { … }

Yes, please :-). I still don’t feel persuaded there’s a problem in explicitly marking „I terminated successfully“, but at least, try doesn’t have that congitive dissonance between what is written and happens, like catch.

Furthermore, a little bit of syntax bike shedding from me: could we place the keyword directly in front of the brace? eg:

fn foo<T>(t: T) -> Result<usize, Error>
where
  T: Display
try {

}

My reasons for that:

  • It is clearer this is about the block of code than about the interaction with caller (eg. the block of code feels like a parameter to the try keyword, or like the try is flow control for it, similar to how else is just before its block.
  • There’s a case for this in C++ ‒ eg int main() try { ... } catch(...) { ... }
  • Feels much more consistent, both with the stand-alone try (catch) block and with other flow controls.

I’m not sure about the next part, but maybe it could even be that:

  • Any block whatsoever could be prefixed by try
  • If a type ascription of the result is needed, it could be done with the turbofish (just to feel the same and make it optional when not needed).
while let Some(value) = iter.next() try::<Result<(), Error>> {
   
}

Why not add an automatic “wrapping” ability for all enum types? I presume we use box keyword to do this.

Please, no. First, the Try trait seems more flexible and picking just on enums seems a bit like a hack. Second, the word Box has a very very different meaning in Rust and reusing it could be confusing.


#148

The advantages i see in the original proposal is not that you use less characters, but that treating the happy path as the normal return path has both pedagogical and ergonomic advantages which I have tried to articulate.

I would be a lot more sympathetic for the proposal if it would really feel like an abstraction and not like syntactic sugar for just ‘Result’.

Just thinking about it, ‘catch’ might even be a bit like Rust’s Haskell-do-notation.


#149

Just regarding the syntax, I think there’s somehow a nice symmetry between:

fn foo() -> R = catch { … }

let r = catch { … }


#150

I’m not particularly concerned about not seeing Result in the return type, any more than I am concerned about not seeing impl Generator in the return type of an async function.

I can’t say that I like the bahaviour of async functions either.


#151

That makes sense. I don’t really like -> catch R { either – but I do like the R bit of it still, though the next part gives me a bit of pause…

…hmm, so I think we should probably aim for consistency here, in any case. That is, either we want to show the “external” type or the “internal” type, but not sometimes one and sometimes the other. This seems to imply:

async fn foo() -> impl Future<T, E> { ... }

or plausibly impl Generator, but hopefully we could make either work.

I think the real question (that I am still wrestling with) is whether having the user write the full return type will help with learnability or not. I could see an argument either way.


#152

It’s worth writing out bigger examples.

fn foo<T>(
    arg1: A,
) -> T
where
    T: Debug,
    T: Send,
{

}

where does the catch go here?

(Another idea: catch fn, analogous to async fn – both ways to transform the body.)


#153

The more I think about this, the less I like it, sorry. :slight_smile:

First of all, I am used to think of a function as 2 parts: signature and behaviour. This proposal moves part of the behaviour into the signature, making it harder to reason about. The proposed fn(params) -> Result<T,E> catch { ... } mitigates it a bit, as it is effectively the same as fn(params) -> Result<T,E> { catch { ... } }, but still offers very little in terms of clarity.

The other problem is that if we are trying to optimize the positive path by saying return n; automatically wraps it into Ok(n) and fail/throw/err "error" returns Err("error"), we are effectively introducing exceptions, so just call them exceptions and use the standard throws and throw, if not even try and catch.

I honestly cannot see any difference. I am pretty sure that with time everyone will write functions with the catch block, effectively bubbling the errors all the way to main(), like exceptions.

I hugely prefer the explicit return Ok(n) and return Err("error") forms.


#154

People already bubble it up to main, with ? and crates like failure. I think that’s OK.

The big difference is the annotation with ?. The huge pain of C++ is exception safety, because an exception can fall just out of everywhere, so you need to be careful about where you can exit the function. If you have a block of code containing no ?, you can be pretty sure it won’t exit there, so you can have temporarily inconsistent data there. Have you ever participated in a heated discussion of three senior C++ developers, arguing if this function provides correct exception guarantees and which these actually are?

Another difference is, you can do .map() or unwrap_or_else on results, but not on exceptions. I like the flexibility of using what is more appropriate at the time.


#155

I think when considering making changes of this scale, not only should we consider the possible benefit to completely new users, we should also consider how it affects existing language users.

  • Does this change add more confusions in deciding what to use?
  • Does this add a “this language is still in flux” impression to potential serious users in the industry?
  • Does this change focus on removing superficial complexity like the number of characters typed while adding internal complexity like additional reasoning around special cases?

In the meantime, we should also keep in mind that we already have a large portion of resources out there in places like StackOverflow. Thus we should ask whether there is negative impact to new users as well:

  • Does this change increase the need to update existing resources to avoid confusion to new users?
  • Does this change add confusion to new users already starting to get used to simple idioms?

As we should break compatibility if we are fixing bugs and soundness issues, if we are adding real expressive power to the language, or making language significantly more complete or consistent, we should consider changes of such scale. Otherwise we should have a second thought.


#156

It’s worth writing out bigger examples.

fn foo( arg1: A, ) -> T where T: Debug, T: Send, {

}

where does the catch go here?

It certainly isn’t as nice like in the case without ‘where’:

fn foo( arg1: A, ) -> T where T: Debug, T: Send, = catch { … }

(Another idea: catch fn, analogous to async fn – both ways to transform the body.)

It feels more focused on the whole ‘fn’ instead of just the result type, but perhaps it’s just a matter of getting used to it.


#157

Exception safety in C++ is both more difficult and easier. Yes, you must assume that most functions might throw, but functions in rust might panic. In C++ there at least is the noexcept operator that statically checks that the expression (function) cannot throw, and neither can its callees.

I think we should not use the terminology of exceptions in rust because in rust these things are not the same – return values and panics are separated. We might one day add a keyword or some other static guarantee for expressions that will not panic, but no keyword is needed for an expression that cannot fail – you know that by simply looking at its type.

If the type of an expression is Result<T, E> then you know it can fail. I don’t like the idea of a special syntax for describing the type of expressions that are also functions (i.e. -> T catch E). Expressions are expressions and types are types – functions are not so special, imo.


#158

+1 on that. That’s why I advocate for (if we even need it) placing the try (or catch or whatever) or whatever in front of the function block, so it acts the same as on any other block.

Furthermore, does it make sense to be able to put it in front of (non-block) expression? Like:

try Box::new(operation()?)

#159
let x: Result<T, E> = wrap { Box::new(operation()?) };

Sure.

Sorry, missed the part about no braces. Yeah, why not? It works with box.


#160

No, I explicitly meant without the braces. What I mean is, can try take general expression and modify handling in that, with a block being special case of expression?


#161

I’m not sure I follow your flow. Could you elaborate on how this proposal would make things different for you?


#162

There’s some prior discussion of this on this futures-await issue. This works now, I have an old branch of futures-await that implements it.