Pre-RFC: Catching Functions

I’m scared that my prediction will actually happen :(

Think about the future: How will this age? What will happen if this turns out to not be simple and flexible enough? Etc.

I’ve given my answers to these questions. What are yours?

1 Like

try, fail and succeed seem like the best keywords. The latter two seem like great function and method names too, though, so hopefully they can stay useable that way

Btw how does that handle e.g. let something: Result<A, B> when let e @ Err(_) is of type Result<C, D>? This is properly handled by ?.

The idea is that the ?/try syntax is sugar and won’t cover all cases. After all, we want to keep Result accessible as a normal value that you can match, and call methods on, and store in data structures. The flip side of that is that you can do everything ?/try do explicitly, without using the sugar.

Which means, if you simultaneously need wacky control flow and error conversion, you can write this (or several variants of it):

if let Err(e) = some_file.read_exact(bytes) {
    break 'label Try::from_error(e);
}

Procedural update:

This has been a pretty incredible, far-ranging discussion so far! However, it also remains a contentious one, and despite 280+ comments the thread doesn’t feel like it’s managing to hone in on a set of core considerations. Which is not surprising – that’s really hard to do in a setting like this one!

What I’d like to propose is the following:

  • We all take a break from this thread and let the dust settle a bit.
  • When we’re ready to take this topic up again, we do two things:
    • Form a small “working group” (WG) with representatives from several different perspectives on this thread.
    • The WG first provides a boiled-down summary of the points made on this thread.
    • The WG works together, publicly, to drill more deeply into the constraints from each perspective.
      • Notably, though, this would not be a free-for-all comment thread, but rather a careful discussion amongst a designated set of stakeholders.
    • The WG lays out one or more proposals, based on that discussion, which would go through the normal RFC process. EDIT: or, of course, the recommendation could be to stay with the status quo.

This may sound heavyweight, but really all I’m proposing is that when faced with a contentious topic like this, rather than continue accumulating hundreds of comments at high velocity, we try to identify representative stakeholders who are committed to “digging deep” together, and have them hash through things in a transparent discussion amongst themselves. With the result being a recommendation which still goes through normal RFC discussion.

This is all part of some broader thinking from Rust leadership about how to increase agility and scale up our teams (see my post on the Libs Team for example). I’d like to see the WG above actually be dedicated more broadly to “Settling Rust’s error handling story for the upcoming epoch”. As such, though, this is a bit blocked on putting out the 2018 Roadmap RFC and some general proposals about team structure (both coming soon!) Hence my feeling that right now would be a good time to take a bit of a break from this discussion, and regroup later.

wdyt?

23 Likes

Sounds great to me.

Assuming the fact that I rarely get a chance to actually write Rust doesn’t disqualify me, I’d be interested in representing the “this just isn’t worth it” perspective.

4 Likes

I’d like to volunteer for such a WG with the “fail/pass/succeed” (let’s have very local reasoning…) + “I can change my mind and listen carefully” (I did that quite a lot in this thread ^,- …) perspective =)

3 Likes

I’ve been thinking about why I’m so resistent to this idea, to see if there were any intuitions rattling around in my head which nobody had voiced. (Perhaps ironically, it was when I got side-tracked and started to dwell on this H2CO3 comment and having to write a subset-enforcement git commit hook that I finally realized what I was missing.)

This is like the idea of adding support for classical inheritance in addition to traits in order to broaden Rust’s utility.

Both Rust’s traits and classical inheritance have their mutual strengths and weaknesses. Yes, traits are almost always a more fitting choice, but there do exist problems where “IS A” really is the right model, rather than “HAS A”. (Not to mention that they’d make it much easier to support FFI for things like Qt which rely heavily on C++'s classical inheritance for their APIs.)

That said, those uses where traits are inferior are niche enough and, if classical inheritance were added, the risk of it being abused by people trained to reach for it is great enough, that the downsides outweigh the advantages.

That’s what this proposal feels like to me. Yes, there may be niches where it shines above all the alternatives… but the cost to the language as a whole is just too great and adding it would be failing to see the forest for the trees.

8 Likes

Another wishlist item I forgot above:

  • It would be nice to be able to change the desugaring of doc tests in such a way that you can use ? in them and all existing doc tests are still valid, even those that use return;.

(Catching functions is one possible solution to that, though there may well be others.)

Warning, bikeshedding ahead:

fn regular_func() -> T {…}
fn? throwing_func() -> impl Try<…> {…}
fn* generator_func() -> impl Generator<…> {…}
fn^ async_func() -> impl Future<…> {…}
fn^* async_generator() -> impl Stream<…> {…}
let regular_closure = || {…};
let throwing_closure = ||?: {…};
let generator_closure = ||*: {…};
let async_closure = ||^: {…};
let async_generator = ||^*: {…};

Option 2:

fn regular_func() -> T {…}
catch fn throwing_func() -> impl Try<…> {…}
gen fn generator_func() -> impl Generator<…> {…}
async fn async_func() -> impl Future<…> {…}
async gen fn async_generator() -> impl Stream<…> {…}
let regular_closure = || {…};
let throwing_closure = catch || {…};
let generator_closure = gen || {…};
let async_closure = async || {…};
let async_generator = async gen || {…};

You can specify concrete type like normal, but it should impl appropriate trait, so the “Ok-wrapping” can work. All functions except regular should do the “Ok-wrapping” and allow throwing

Of those two choices, I definitely think option 2 is more in line with Rust’s existing design principles for several reasons:

  1. The history of Rust’s evolution prior to the 1.0 freeze was a history of moving away from sigils.

  2. The sigils chosen are counterintuitive.

    Given Rust’s the existing C-inspired use of * as the dereference operator and as the recognizable common component of the *const/*mut syntax for creating raw pointers for FFI use, I’d assume fn* meant some kind of pointer-indirected function declaration if I saw it in Rust.

    (Or perhaps some kind of analogue to #[repr(C)] for functions if I forgot that extern "C" existed.)

    Likewise, given that Pascal uses ^ for pointers in a manner similar to how C uses * and Pascal was used quite widely as a teaching language for a long time, the same concern applies for ^. (Not to mention that Borland Turbo Pascal was the way to write performant, typesafe code in the DOS era and Apple’s Object Pascal was to classic MacOS what C was to UNIX.)

    For example, Pascal’s “assign a value to the target of a pointer” syntax is pointer_var^ := 1. (Pascal uses the := mathematical “is defined as” syntax for assignment rather than =)

  3. Option 2 is better for learnability, because you can look up alphanumeric keywords in an ordinary web search while sigils tend to get stemmed away, requiring prior knowledge of the existence of the Rust syntax index. (It’s also easier to intuit the meaning of keywords like async and gen without resorting to a reference guide and to memorize them quickly.)

2 Likes

Option 1 is better for backwards compatibility.

Option 0 (“this is unnecessary”) is also better for backwards compatibility, and doesn’t require you to learn 30 function types like in other languages. You still need to learn some types (as in type-system types, like Result or Option) but you’d have to learn them anyway.

Alternatively we could have macros in trait item/trait impl item position, so catch! and stuff could be used without breaking backwards compatibility.

I was able to learn rust in a week because it has this syntactic simplicity. It’s that important to me.

6 Likes

Excellent solution to the function-level-or-not ambiguity :+1:

I would actually prefer Option 3:

fn regular_func() -> T { ... }
fn throwing_func() -> Result<U, E> { /* 1) */ }
fn generator_func() -> |T| => U { /* 2) */ }
fn async_func() -> |T| => () -> U { /* 2) */ }
fn stream_func() -> |T| => U -> V { /* 2) */ }

let regular_closure = |T| -> U { ... }
let throwing_closure = |T| -> Result<U, E> { ... }
let generator_closure = |T| => U { ... }
let async_closure = |T| => () -> U { ... }
let stream_closure = |T| => U -> V { ... }

The syntax should be self-explanatory: a closure taking some initial parameters |T|, yielding => 0-n times a value of type U, finally returning -> a value of type ‘V’.

ad 1) I don’t believe that Ok-wrapping is a sufficient reason for special-casing Return return types. That said, a natural extension to above proposal could be:

fn throwing_func() -> T? { ... }

mimicking the ? in code used to exit early. However, may be confusing with optional values in other languages.

ad 2) This is for functions returning a closure. Functions that themselves are closure-like should either be assigned as closure let closure = |T| => U -> V. Alternatively, for top-level declarations, combining closure and fn:

fn closure_like_func |T| => U -> V

1 Like

I am strongly against the -> T catch E and the implicit Ok wrapping because it hinders my capacity to comprehend code when I’m reviewing code using those features. I do not care about “editing distance”, I care about not having to remember that foo may actually compile to Ok(foo) in some circumstances, based on code that is not closely at the same place.

I don’t understand what is wrong with writing Ok(7) instead of 7, nor why T catch E is better than Result<T, E>. Why reuse exceptional terminology when you can use plain old values everywhere? Having the concept of fallibility reified is to me a very important feature of Rust, I disagree that this proposal retains this value. I am also against catch { ... } expressions doing any sort of Ok-wrapping for the same reasons.

I hope Rust doesn’t get any of the features listed in this pre-RFC.

16 Likes

I think ! would be nicely symmetrical with ?:

let x: Result<T, E> = try {
    let y = f()?;
    if g() { throw e; }
    y!
};

But I suppose it would lead to confusion with macros, if it's even syntactically possible.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.