Pre-RFC: Catching Functions

maybe expect Ok { // start function body or default Ok in { // start function body

1 Like

I agree the word catches fits less well. I am mildly worried that we are "inverting" the sense of catch anyway -- I know it makes sense from a certain perspective, but I still think it will surprise people.

3 Likes

What about?

fn foo() -? io::Result<()> { /* stuff */ }
1 Like

This might confuse people from other languages (like Java, C++), where something like

try {
  ...
} catch (FooException fe) {
  ...
}

only catches FooExceptions. In contrast, this construct catches all early returns and just specifies their type.

I really like the proposal for

fn foo() catch R { ... }

I'm not a big fan of ok-wrapping (I like explicitly writing Ok(t)), but I think having the catch in the signature helps. I like having the whole type and not hard-coding in Result. (Also, perhaps we can omit the -> if there is catch?)

@bbatha @Centril Mixing -> and ? seems to result in some pretty weird-looking syntax IMHO. I would actually like a keyword for this, which is more syntactically obvious to me.

Not a fan of omitting ->, I don't even like that you can make fn main() -> () into fn main() (but we can't remove that ability now..). There's certain beauty in function arrows for functional programmers since it looks like math =)

I’m trying to think about the best way to explain why that feels different; please pardon the smoke coming out of my ears as I think really hard to introspect that feeling. :slight_smile:

If I see something like this:

fn foo(s: String) -> &str {
    // ...
    {
        // ...
        return &s;
    }
    // ...
}

I don’t see the change of &s from &String to &str as part of the return, I see it as part of the &s, and a fairly natural one; effectivly, &str is what you typically get when you apply & to String. That doesn’t feel like magic attached to return, it feels like magic attached to &.

8 Likes

I’m assuming something like

fn foo() -> u32, Error { 
  if bar() {
    return 0;
  } else if baz() {
    throw MyError;
  }
  7
}

wouldn’t work ?

It’s kind of both both, isn’t it? &String only gets auto-derefed to &str because it’s used in a context that expects &str. That is, let s = &string produces s: &String while let s: &str = &string produces s: &str.

I did like the comparison with C# async, which retains the full return type in the signature but changes the meaning of return in the body. I think whatever syntax we end up with should probably emphasize that it’s a body-level transformation and not part of the function signature.

Perhaps rather than thinking about Ok-wrapping as a coercion it would be better to think of it as a wrapping behavior. I certainly don’t want both return 0 and return Ok(0) to work in the same context, but I do like the idea of catch blocks and functions letting you write non-Result-y code internally.

2 Likes

I feel like this has the same problem as T catch E: there doesn’t appear to be an obvious generic way for this to work with any return type R: Try…

maybe that isn’t a deal-breaker kind of a problem ? In the sense that we rarely see in languages with exceptions, people asking for a way to change the vm-internal types handling the successful return and erroneous exceptions ? :slight_smile:

That is fn foo() -> u32, Error being sugar for fn foo() -> Result<u32, Error> which would be the most common use case, but one might still use return and throw in a fn foo() -> Try<u32, Error> if one needed to, just like the C# async example Niko showed ?

Hmm… -?> doesn’t look all that weird to me… I think terseness here aids in bribing people away from .unwrap()…


But here’s perhaps a better idea:

Why not replicate the proposed throw / fail with succeed semantics intead?

So let’s introduce:

fail $expr <=> return <return_type>::from_error(From::from($expr))

and

pass $expr <=> return <return_type>::from_ok(From::from($expr))

// both fail and pass are 4 characters long, as a nice aesthetic bonus =P

By doing so, we don’t have to change anything in the function signature, which is nice. It also enables very local reasoning about the semantics.

Reworking the original example:

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

   quux()?;

   pass 7
}

Some potential keywords instead of pass:

  • succeed
  • ok
  • deliver
  • triumph (OK; this is a joke…)

(yeah, I totally checked a dictionary… :smiley: )

11 Likes

Hmm… I actually quite like that proposal :slight_smile:

I think ok is nicer than pass, but it might cause confusion with Result::Ok… not sure what others think, though…

Thanks =)

I first wrote my comment with ok, but then realized that the fact that both fail and pass are 4 characters long makes for very nice symmetry and beauty wrt. alignment, so I went with pass.

Since the Try trait has not been stabilized yet, we can also change the names of the associated types into Fail and Pass and methods into from_fail and from_pass to make things more consistent.

To continue the bikeshed… I think pure might be an even better name than pass and corresponds well to:


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

   quux()?;

   pure 7
}

Using pure also has the advantage of not confusing python devs. Another advantage is that pure is a reserved keyword already.

N.B: In Haskell pure :: Monad m => a -> m a has no early return for success like this since the language is not imperative, so this would be a difference, but I’m sure Haskellers (like myself) could live with this :wink:

EDIT: The folks over at #haskell @ freenode did not like using pure for this - so I am now sure they could very barely live with this. So scratch pure as a contender here imo.

This thread is quite long. I haven’t read beyond the first 10 replies. I suspect someone has made my points, but here goes…

I really like the concept of throw expr. Coming from someone who’s point of view is mainly object oriented (and mostly Ruby), I find that syntax to be much more clear what’s going on than Err(expr)? (tbh I hadn’t even considered that as a possibility until reading it here. I’ve always written return Err(expr.into()).

I really dislike the idea of -> T catch E. I do not think that syntax obviously maps to Result<T, E>, and from the perspective of someone reading some Rust code who encounters that syntax for the first time, it is not going to be something that is easily googleable. Additionally, I do not like the idea of the return type of a function affecting the behavior of return 1 (or even just 1 in some cases). This feels really non-local to me. Having to potentially scroll in my editor to determine what an expression does puts a bad taste in my mouth. (Yes, this is partially a straw-man, since that is already the case today with type inference, especially if the last line of the function is x.into(), but I consider that to be generally bad practice and “different”).

10 Likes

Can you show a more concrete example of how other return statements would need to change when a function becomes fallible? I could see wanting to review and revise the entire function if it starts doing something fallible (though I also think there are many cases in which this would be an excess of caution), but nothing about the return statements in particular seems like they should be highlighted.

This is quite an example because of course we use the return type of a function to determine what return 1 means - we determine the type of 1 from the return type of the function. As has already been mentioned, we also perform deref coercions (which can run arbitrary code) and other coercions like this.

So could you move from arching statements like "the return type of a function should not affect the behavior of return expressions" - which are false regarding Rust as it exists today - to more concrete statements about why wrapping them in Ok is problematic?

So could you move from arching statements like “the return type of a function should not affect the behavior of return expressions” - which are false regarding Rust as it exists today - to more concrete statements about why wrapping them in Ok is problematic?

Well I didn't say that, and I explicitly said in my reply that there were many cases that we do this which don't bother me for reasons I'm not sure how to quantify... But I guess I can try to figure out why this bothers me but deref coercions don't...

Every example you've raised is an example of type inference, which happens universally in the language. 1 always infers to u16, regardless of it's a return type or a type parameter. Same for deref. Same for into. This would be the only case where a coercion like this is not tied to type inference.

I think inferring integer/float types is very different from the other arguments. It's quite easy for me to see 1 as "oh yeah that's an integer of some unspecified size". Not to mention that inferring 1 as 1u16 vs 1i32 does not fundamentally change its meaning. Every other one of your examples has something at the call site which implies conversion could be happening. Deref requires &. Into requires .into.

1 Like

Thanks for further introspecting on what bothers you about it! I’ll think about your response more thoroughly, but I want to quickly correct one factual point: deref coercions are not type inference. You’ve returned an expression &String and the function expects &str; we’re not actually inferring the type of you’re return expression, we’re inserting code based on the relationship between that type and the type of the return value.

1 Like

I’ve always thought of it as a form of type inference, especially since it doesn’t apply to trait dispatch (one of the only places type inference doesn’t apply), but that makes sense as a difference. Unfortunately my arguments aren’t super technical, they’re that my squishy feelings like one thing but not another. :slight_smile:

4 Likes