Inline error recovery

There should be an !? operator, similar to ?, but that does it on the error case instead.

? is akin to and_then, !? is akin to or_else, the operator comes from applying the ! operator to the ? operator itself.

Usage would be some_option!? and some_result!?.

Problems come with Result's insistence on applying into() to the Err, but we can safely just ignore that and force the user to explicitly specify the desired type when using !?. Oh yeah, it must be written as let _: Foo = result!?; when the Err is discarded, because the into() exists whether you use the value or not.

In Rust, a postfix ! is familiar as macro invocation, and is not familiar as negation. While I can't think of an existing syntax that technically overlaps with this, it doesn't seem consistent.

I also don't see sufficient motivation for a new operator, much like people have been saying in your other very similar thread here.

5 Likes
loop {
  let _: [omitted] = self.inner.close()!?;
}

doesn't look like a macro to us.

What is wrong with having a macro for this? As we did first with try?

4 Likes

It doesn't even need to be a macro: the simplest way, building on the ideas from the other thread, would be to make an extension trait that adds a flip() method to Result, and use .flip()? wherever you would use the hypothetical !?.

3 Likes

!? doesn't flip tho. it's more the ? equivalent of unwrap_err. which is a thing. we have unwrap_err today. for some reason. but not an ? equivalent of it.

The flip method would swap the Ok and Err variants, so foo.flip()? would early-exit if foo is Ok, while evaluating to the Err value if foo is Err. Isn't that exactly what you intend !? to do? If !? isn't equivalent to .flip()?, then I have no idea what it's intended to do.

2 Likes

The flip method would swap the Ok and Er variants, so foo.flip()? would return Err if foo is Ok.

The !? would return Ok if foo is Ok.

The !? is the Result equivalent as || is to bools, akin to how ? is the Result equivalent as && is to bools.

Oh, I see. That is indeed something you'd need a macro for.

As for the !? notation, I expect that many people would assume the same meaning I did for it (if they even understand it as a negation of ? at all), which means it would be a thoroughly confusing notation.

2 Likes

I don't think a sequence of characters which (in English) is meant to denote bewilderment is going to effectively communicate anything other than bewilderment to the underexperienced.

11 Likes

Agreed, featuring .or_else semantics using a pattern that is currently visually associated with .and_then has kept me confused for a while until I figured it out.

Sigils only come after a pattern when it becomes pervasive enough to warrant the readability impact that the shorthand introduces.

To me, your use case would be covered by a much more generally applicable and yet still readable pattern: that of "the elvis operator" / guard let:

// imaginary syntax
let Err(err) =? self.inner.close() else return Ok(());
stuff(err);

for your Ok(()) case, which could be generalized to allow pattern-match destructuring the remaining patterns on the else branch:

// imaginary syntax
let Err(err) =? self.inner.close() else match { Ok(ok) => {
    return Ok(ok);
}};
stuff(err);
  • or with in (assigning) patterns:
    let err; match self.inner.close() { Err(in err), Ok(it) => {
        return Ok(it);
    }}
    stuff(err);
    

In the meantime, a macro is the go-to approach for a verbose pattern within certain code that not all the Rust users would work with:

macro_rules! unwrap_err_else_ret_ok {( $result:expr ) => (
    match $result {
        // TODO: properly qualify the variants since they lack macro hygiene.
        Ok(ok) => return Ok(ok /* .into() */),
        Err(err) => err,
    }
)}

loop {
    let err = unwrap_err_else_ret_ok!( self.inner.close() );
}

I have really tried to use a short macro name, but for it to be a readable one, I find that those 5 words (or synonyms of them) ought to be in the macro name for its semantics to be clear to a reader. Combined with the fact I have not really encountered this pattern in the wild (except maybe with an explicit match when it was necessary), suggests that a sigil should not be used to replace a described-with-5-keywords macro.

hmm what about ||? then, as it's analogous to ||.

or just make && and || use Try. :v

You're looking for the interrobang, which is the '‽' symbol (unicode U+203D). Since it's a single character, a large amount of parsing-type questions go away. It also means that you don't have any issues with people accidentally deleting either '!' or '?', leaving behind something valid, or putting a space between the two, making things confusing (the Damerau-Levenshtein distance is greater when using ‽ instead of ? or !). Since rust assumes Unicode by default, the compiler should be able to parse it fairly easily, the only requirements are the normal ones when someone adds a brand-new operator to the language.

The two drawbacks:

  • As far as I know, ‽ is not mapped natively on any keyboard. Every single person out there is going to have to learn how to map keys to symbols in various operating systems. This will likely have to be made a part of the FAQ because it will be the second most common question asked about rust as soon as the symbol is added to the language.
  • The most common question will be 'what is that weird question mark/exclamation point symbol, and what does it mean in rust?!' There will have to be a FAQ on the landing page for the rust-lang.org that explains it in the first 3 words because every forum on the planet is going to have people asking about it...
1 Like

it's still more that 24h until April 1st calm down.

but if I understood you correctly you are proposing a try_err macro?

1 Like

Actually, try trait v2 will allow you do define a method for something like this yourself (even in an external crate). You would need to write something like baz().prop_ok()? instead of baz()!? but that’s probably way more readable anyways. [“prop” is short for “propagate” in the method name I use here.]

I have a proof of concept implementation, allowing for

fn foo() -> Result<u32, bool> {
    Ok(42)
}

fn bar() -> Result<(), char> {
    Ok(())
}


fn baz() -> Result<u32, char> {
    let x = foo().prop_ok()?;
    // x is bool, but no type annotation needed
    bar()?;
    Err('e')
}

(playground)

3 Likes

ooh, try trait v2 looks very nice.

And to be fair I could definitely see a use for something like this. But I would prefer it as you wrote it not as a new sigil.

If you're talking about the interrobang, I'll admit it's unusual, but it wasn't meant as a joke. APL used mathematical symbols in the language directly, which lead to the use of specialized keyboards just for APL. Given that it is possible to remap keys on your keyboard, it would be entirely possible to map the interrobang to something that is easy to type, but not currently in use.

All that said, I do agree with both of you that it's not the best choice to use here. Even with remapping keys, it's still too hard to type.

1 Like

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