Is the ? operator a pre-processor command to the compiler?

impl fmt::Display for Pet {
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        f.write_fmt(format_args!("{}", self.border))?;
        write!(f, "({},{})", self.animal, self.amount)
    }
}

I understand that the '?' operator is used as short hand to return the Error from the fmt::Result if it occurs before trying to proceed to the write! macro.

This would imply that the ? operator is some kind of pre-processor/macro run to rewrite the code before the compiler runs, otherwise how would the compiler know about a specific Enum written in Core:

core::result
pub enum Result<T, E>

The compiler would have to be hard coded to interpret the ? to go looking for Result but it self compiles the code for Core.....

Also, this smells a bit like chaining monads together in Haskell etc. Is that right?

Thanks.

The ? operator de-sugars (for Result) to something like:

match expr {
    Ok(v) => v,
    Err(e) => return Err(e.into()),
}

Everything else is compiled normally. So all you need is a From<ExprErrType> for FunctionErrType.

This is exactly that.

1 Like

thank you. I've noticed in all the literature I've read so far that Rust stays clear of specifically using the 'monad' word. I guess this was deliberate as it could scare people!

No, it isn't a "pre-processor command". If you are thinking about the C preprocessor, Rust doesn't have anything like that. (It does have a macro system, which is much more complex and way less error prone than crude textual subsitution.)

Note that you don't need to know about the specific type in order to de-sugar this operator. In fact ? is not tied to Result, it works with any type that implements the (currently unstable) corresponding trait. For example, it also works with Option.

It's similar, but Haskell doesn't implement such monads by returning early. The ? operator pretty much does a regular return from the innermost function. In contrast, the implementation of >>= for Either and Maybe in Haskell is a conditional which simply doesn't call the continuation upon failure; it can't perform literal early returns, although the do sugar does make it look similar.

2 Likes

In Rust, you'd find yourself running into Monads directly farily rarely, since you have both the option of impure functions and mutability. On the other hand, Monads are such a general pattern that you'd find them popping up in all sorts of places.

1 Like

The ? operator is syntactic sugar which does get translated fairly early in the compilation (but not into actual source code text like a pre-processor would operate). As such, it is comparable to other things like how for loops are syntactic sugar, too.

A for loop is equivalent to a loop expression containing a match expression as follows:

'label: for PATTERN in iter_expr {
    /* loop body */
}

is equivalent to

{
    let result = match IntoIterator::into_iter(iter_expr) {
        mut iter => 'label: loop {
            let mut next;
            match Iterator::next(&mut iter) {
                Option::Some(val) => next = val,
                Option::None => break,
            };
            let PATTERN = next;
            let () = { /* loop body */ };
        },
    };
    result
}

The desugaring of ? is a bit vague, because its stable use cases are only for concrete types, while the actual desugaring uses a (still unstable) trait infrastructure within which, EXPR? desugars to

match Try::branch(EXPR) {
    ControlFlow::Continue(v) => v,
    ControlFlow::Break(r) => return FromResidual::from_residual(r),
}

since this still unstable and thus an implementation detail, it might be more accurate to just give equivalent desugarings for the concrete (stable) types that are supported, so e.g. something like

match EXPR {
    Ok(v) => v,
    Err(e) => return Err(From::from(e)),
}

for Result, or

match EXPR {
    Some(v) => v,
    None => return None,
}

for Option.


Another thing that sets apart such syntactic sugar from preprocessors is that error messages can often handle the syntactic sugar in a nice, unexpanded manner. E.g. if you try something like

fn foo(x: Result<(), i32>) -> Result<(), u32> {
    Ok(x?)
}

you get an error message that’s aware of the fact that a ? operator was involved

error[E0277]: `?` couldn't convert the error to `u32`
 --> src/lib.rs:2:9
  |
1 | fn foo(x: Result<(), i32>) -> Result<(), u32> {
  |                               --------------- expected `u32` because of this
2 |     Ok(x?)
  |         ^ the trait `From<i32>` is not implemented for `u32`
  |
  = note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait
  = help: the following other types implement trait `From<T>`:
            <f32 as From<i16>>
            <f32 as From<i8>>
            <f32 as From<u16>>
            <f32 as From<u8>>
            <f64 as From<f32>>
            <f64 as From<i16>>
            <f64 as From<i32>>
            <f64 as From<i8>>
          and 67 others
  = note: required because of the requirements on the impl of `FromResidual<Result<Infallible, i32>>` for `Result<(), u32>`

as you can see in the lines

error[E0277]: `?` couldn't convert the error to `u32`

and

  = note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait
4 Likes

Sortof yes, but also completely not.

The implementation of ? doesn't know anything about Result, the same way that the implementation of + doesn't know anything about num::BigInteger.

Here's where the compiler turns ? into calls to various trait calls and matches:

So you might think of that as being "macro-like", though it works on the AST and not on tokens the way macros work in Rust. (And not at all on blind text like C's macros work.)

Notably, if you consider ? to be a macro, then you'd also have to consider for to be a macro, because it also disappears in the same phase of the compiler:

3 Likes