Bikeshed: A consise verb for the `?` operator

I won’t lie. This has been a really amusing thread to read :stuck_out_tongue:

9 Likes

From Ferris the crab to turbo-fish to bubbling, rust is really going aquarium.

12 Likes

Given that theme we also could call it the hook operator. catch blocks would also work in that theme...

5 Likes

I really like bubble.

It‘s cute, descriptive and and has a nice flow for vocalizing code (create file f, bubble, write to f, bubble, …).

Also, ? looks a little bit like a rising bubble :wink:

5 Likes

@gbutler, @phaylon, @repax, @Centril

How about:

let _: Result<Foo, Bar> = proc {
    foo()?;        // confirm operator, Try trait, like "confirm to continue"
    halt Bar(0);   // halt operator, implemented with TryBreak trait, exits block
    halt Baz(0);   // halt operator, implemented with TryBreak trait, jumps to recover
    Foo(0)         // return from block, override with TryContinue trait
}
recover Baz(x) {   // pattern match on `halt`s in `proc`
    bar()?
    halt Bar(1);
    Foo(1)
} // value of recover block must be convertible to Result<Foo, Bar>
// ...

The theme of the above vocabulary is “control flow”, which enables the data’s terminology to shine through.

  • proc - already a reserved word, fits description of body (a procedure)
  • confirm - suggestive of early-exit, short for “confirm and continue”
  • halt - pairs well with proc and recover, explains action without being error-centric.
  • recover - pairs well with error handling (recover io::Error) and the control flow.

Bubble is kind of nice. But can you only “bubble” errors? There’s nothing inherently erroneous with bubbles.

@illustrious-you As mentioned by @steven099, proc isn’t that clear on what it’ll do in the case of a return, which I think is a valid concern. Perhaps if we could find a term that doesn’t seem to capture the return value, but on the other hand do capture break values. That would be great. You might want to short-circuit the block with a success value, too.

break/fix:grin: Edit: Actually, splint might be more appropriate, since it represents the idea something is still broken, but is being held together in one piece.

And yeah, bubbles definitely aren’t erroneous. However, going with ‘bubble’ provides the opportunity to call the feature ‘bubble/wrap’. ‘Bubble/trap’ would make more sense, though… But I’m still more in favour of using established terminology.

1 Like

Oh my; there’s some truly unbounded creativity going on here :stuck_out_tongue:

1 Like

Two things come to mind:

  • halt might be misinterpreted in a systems language. Maybe just break and make recover a scope you can break out of without a label?
  • What would halt do inside of recover?

@phaylon

What would halt do inside of recover?

The same thing it does in proc. Set the break value of the proc block to the halt's argument.

@steven099, @repax:

let _: Result<Foo, Bar> = routine {
    foo()?;           // confirm operator, Try trait, like "confirm to continue"
    halt Bar(0);      // halt operator, implemented with TryBreak trait, assign Err(Bar(0)) to _
    halt Baz(0);      // halt operator, implemented with TryBreak trait, jumps to recover
    continue Foo(0);  // early return from block
    Foo(0)            // return from block, override with TryContinue trait
}
fix Baz(x) {          // pattern match on `halt`s in `proc`
    bar()?
    halt Bar(1);      // assign Err(Bar(1)) to _
    continue Foo(0);  // assign Ok(Foo(1)) To _
    Foo(1)
} // value of recover block must be convertible to Result<Foo, Bar>
// ...

You might want to short-circuit the block with a success value, too.

Added continue, but that doesn't really describe what's gong on.

Certainly, you can bubble None as well.

It seems like, if ’break with value’ is a general feature, and there’s Ok wrapping, then break should actually represent the success case. In which case, you want something really clear like fail to represent the error case (fail matches try {}).

@steven099

It seems like, if ’break with value’ is a general feature, and there’s Ok wrapping, then break should actually represent the success case.

I don't know of a case where break does ok-wrapping.

In which case, you want something really clear like fail to represent the error case (fail matches try {}).

With fail, and a new keyword pass for ok-wrapping:

let _: Result<Foo, Bar> = routine {
    foo()?;           // ? operator
    fail Bar(0);      // assign Err(Bar(0)) to _
    fail Baz(0);      // assigns Err(Bar(x)) to _ after error conversion
    pass Foo(0);      // assign Ok(Foo(0)) to _
    Foo(0)            // assign Ok(Foo(0)) to _
}
// ...

This seems to have worse cohesion than the earlier blocks, I can't think of a name for ? in this pattern, and it explicitly returns to an error handling construct. On the other hand, it can't be argued that it behaves anything like traditional exceptions.

There may be a way to tie in with the pass/fail terms:

let _: Result<Foo, Bar> = eval { // like "evaluation"
    foo()?;       // try operator
    fail Bar(0);     
    pass Foo(0); 
    Foo(0)          
}
// ...

Since evaluate and try have similar meaning in this context, they can be swapped:

let _: Result<Foo, Bar> = try {
    foo()?;       // evaluate operator
    fail Bar(0);    
    pass Foo(0);
    Foo(0)         
}
// ...

There isn't anywhere that does ok-wrapping. If catch (or try or...) and maybe certain functions were to do ok-wrapping, it would be something that happens at the block/function boundary. break represents a block boundary, and thus it would make sense for ok-wrapping to happen there.

Again, I like catch { ... } with throw, where ? can be 'try'. But for try { ... } with fail, then ? can be 'branch' or 'maybe' or something.

let _: Result<Foo, Bar> = catch {
    foo()?;          // try or bubble?
    fail Bar(0);     // break w/ err-wrapping, replaces throw
    fail Baz(0);    // like `goto handle` w/ handle identified via pattern match.
    pass Foo(0);     // break w/ ok-wrapping
    Foo(0)           // implicit ok-wrapping
}
handle Baz(0) {      // must break with either Ok(Foo) or Err(Bar)
    fail Biz(1);     // break with Err-wrapping
    pass Foo(1);     // break with Ok-wrapping
    Ok(Foo(2))       // return explicit value (no ok-wrapping)
}

And the eval-based example:

let _: Result<Foo, Bar> = eval { // like "evaluation"
    foo()?;         // try operator
    fail Bar(0);    // break w/ err-wrapping, replaces throw
    fail Baz(0);    // like `goto handle` w/ handle identified via pattern match.
    pass Foo(0);    // break w/ ok-wrapping
    Foo(0)          // implicit Ok-wrapping
}
handle Baz(0) {      // must break with either Ok(Foo) or Err(Bar)
    fail Biz(1);     // break with Err-wrapping
    pass Foo(1);     // break with Ok-wrapping
    Ok(Foo(2))       // return explicit value (no ok-wrapping)
}

I believe the handle semantics could be useful in solving the problem RFC 243 points out about match sugar:

There was some concern about potential user confusion about two aspects:

  • catch { } yields a Result<T,E> but catch { } match { } yields just T;
  • catch { } match { } handles all kinds of errors, unlike try/catch in other languages which let you pick and choose.

The handle logic presented above runs on fail, and can pattern match on the type of any fail, but they must return a value consistent with the statement's type (in the above examples, handlers must return Result<Foo, Bar>). The fail and pass shortcuts are still available, but fail handlers don't recurse, so the type returned in fail must have a std::convert::From implementation to the expression's failure type.

TBH, I laughed when I read that the first time. But now I love it :heart:

It's pronounceable & distinctive. But more importantly, it actually manages to imply both conditional and propagation, while most of the others (vomit, halt, ...) only imply "it failed" to me.

6 Likes

I use it to ensure that value is not error

3 Likes

A post in the dedicated operators thread has me thinking about ? some more. A thought experiment – if ? were allowed to be a user-defined trait, what would it be called? For me, I keep coming back to verbs which have a nice adjectival form:

  • check, checked, checked expression, Check trait
  • guard, guarded, guarded expression, Guard trait

I actually really like check. To my ears, it carries the feeling of “open and examine for correctness”, while also being terse and work well in adjective form. Guard, on the other hand, doesn’t carry that sense of examining to me. (and probably conflates the issue with guard clauses, if they were to come along.)

Other interesting verbs like check that come to mind: vet, verify, examine, inspect.

impl Check for Result {
    fn check(self) {
        match self {
            Err(e) => return 'super Err(e.into()),
            Ok(v) => v
        }
    }
}

(imagine, for the purpose of this experiment that the check function could actually return from the enclosing scope. Perhaps if traits could implement macros?)

NB: I am pretty new to Rust, so take this for what it’s worth! I guess that could be a positive or a negative, depending on your perspective! :slight_smile:

4 Likes

Not a verb, but I’d read it as please?.

fn main() -> std::io::Result<()> {
    // Create "foo.txt", please?
    let mut file = File::create("foo.txt")?;
    // Write "Hello World!" to it, please?
    file.write_all(b"Hello, world!")?;
    Ok(())
}

It makes sense to me that it wouldn’t be a verb, because verbs are for methods/function. The please? operator qualifies the action that’s taking place.

Granted, this doesn’t convey the “propagation” part of the story, that bubble does very well.