Virtual-Binding-Based "Error Propagation Operator" 🤯

In Rust, if you define your function interface as possibly returning an Error, then you might find a circumstance where you call a function whose error you wish to propagate up to become your function's error.

Here's how you'd traditionally write something like that, in a case where it potentially propagates an error from one of two calls:

fn read_username_from_file_traditional() -> Result<String, io::Error> {
    let f = match File::open("username.txt") {
        Ok(file) => file,
        Err(e) => return Err(e),
    };
    
    let mut username = String::new();
    match f.read_to_string(&mut username) {
        Ok(_) => Ok(username),
        Err(e) => Err(e),
    }
}

But there's an application of "the ? operator" (which doesn't really have another name) which will do this propagation for you.

fn read_username_from_file() -> Result<String, io::Error> {
    let mut username = String::new();
    File::open("username.txt")?.read_to_string(&mut username)?;
    Ok(username)
}

Historically We Couldn't Do Things Like This

There was no way for such an operator to be able to know what the concept of RETURN was.

Hence you see things like:

change: combinator [
    "Substitute a match with new data"
    return: [~<change>~]
    parser [action!]
    replacer [action!]  ; !!! How to say result is used here?
    <local> replacement'
][
    [^ remainder]: parser input except e -> [  ; first find end position
        return raise e
    ]

    [^replacement' #]: replacer input except e -> [
        return raise e
    ]

    ; CHANGE returns tail, use as new remainder
    ;
    remainder: change:part input (unmeta replacement') remainder
    return ~<change>~
]

But Virtual Binding Means We COULD Do It

Let's say we were using ? for this also. It could essentially have this behavior at the callsite:

let result: meta some-call xxx yyy zzz
if raised? unmeta result [return result]
result: unmeta result

So you could rewrite the above as:

change: combinator [
    "Substitute a match with new data"
    return: [~<change>~]
    parser [action!]
    replacer [action!]  ; !!! How to say result is used here?
    <local> replacement'
][
    [^ remainder]: ? parser input  ; first find end position
    [^replacement' #]: ? replacer input

    ; CHANGE returns tail, use as new remainder
    ;
    remainder: change:part input (unmeta replacement') remainder
    return ~<change>~
]

That's awesome. I don't know that I love the ? for the name, I'd probably like !! better.

    [^ remainder]: !! parser input  ; first find end position
    [^replacement' #]: !! replacer input

Though as the comment above shows, I use !!! for attention. One exclamation point?

    [^ remainder]: ! parser input  ; first find end position
    [^replacement' #]: ! replacer input

That's too slight. Something out of left field... maybe a visual indication of "pass this error up?"

    [^ remainder]: --^ parser input  ; first find end position
    [^replacement' #]: --^ replacer input

Or something to indicate what you're passing up, like * for "problem"?

    [^ remainder]: *--^ parser input  ; first find end position
    [^replacement' #]: *--^ replacer input

Interesting looking ideas... but would be a caret-in-word exception. :frowning: (There's not--I suppose--any particular reason why you can't have carets in words if they're not at the head...we allow it for tick marks.)

Nuance of Slashes I Realized

I've mentioned the goal that if you write something like not/even? that would act as if you had written not even?. I presumed that wouldn't be useful typically, but it would be useful if you were using a terminal slash, and trying to pass the cascade of functions as a single value somewhere, e.g.

 >> match not/even?/ 7 
 == 7

BUT there's a rule in infix deferred processing that it runs "one expression evaluation to the left".

This means ?/foo could be used to not disrupt the infix deferred logic. It's more general than just this case, which is awesome. But it definitely helps here.

I'll Write It Up, But It Needs a Name...

Suggestions, please!

It is great! I love that Ren-C has become a language where you can do this.

(And personally, I’d be fine with calling it ?.)

I think ‘error propagation operator’ is a fine name. But somehow, I suspect that name would be a little too clunky for your tastes…

1 Like

Still a lot of questions to be answered, but it really is night and day from the historical binding model.

One question to answer with ? though is what to do about it when the concept of "what to call to propagate" isn't named RETURN.

Though as it so happens, generators aren't allowed to YIELD a raised error. There is only one raised error contractually that arises from a generator, and that's the generation termination error. This way if you intercept a raised error from a generator you can assume the reason why and act on it. All other raised errors will be promoted to abrupt failure before the YIELD can give them back.

But maybe you have a CATCH/THROW situation and you want the throw to be what you use.

The just-a-thought-for-now concept of dialecting function calls might have some options here. What about ?:throw

result: catch [
   ?:throw some expression
   ?:throw some other expression
   print "Then you get here"
   throw <success>
] except e -> [
   print ["You failed!" mold e]
]

So it could presume RETURN if you don't say otherwise, yet still be used for other things.

The dialected function call concept has been simmering a bit, and I do keep finding places where I think it would be nice.

Prior to that being decided, there could still be a convention to name your own variants, e.g. ?-THROW

result: catch [
   ?-throw some expression
   ?-throw some other expression
   print "Then you get here"
   throw <success>
] except e -> [
   print ["You failed!" mold e]
]

And you could define it within a context if you planned to use it a lot:

result: catch [
   let ?: ?-throw/
   ? some expression
   ? some other expression
   print "Then you get here"
   throw <success>
] except e -> [
   print ["You failed!" mold e]
]

Though I will make the not-insignificant-point that you can beat this by wrapping your generator up to use meta/unmeta.

Basically wrap GENERATOR such that your version's YIELD does a meta-yield, and that the generator itself is adapted to unmeta the result.

You will have successfully built a generator which can conflate raised errors with the no more to generate error.

(If it turns out people actually find they want to do this commonly, it could just be a refinement. GENERATOR:CONFLATE or something.)