It’s covered in one of the first lessons in the Rust book, which includes many examples for how it can be used.

In Rust, ? is a special operator that is commonly used to signal an early return if an error is encountered. It’s a killer feature of Rust because of how much it improves readability of code, allowing us to write concise code while handling errors.

The original implementation of the ? operator was syntactic sugar for writing

match <expr> {
    Ok(val) => val,
    Err(err) => return Err(From::from(err)),
}

as <expr>?. You might recognize this as shorthand for writing the Go equivalent for error handling

val, err := <expr>
if err != nil {
    return nil, err
}

There’s a proposal to address error handling boilerplate in Go 2.

Using ? in Rust code is great for removing a lot of error handling cruft that might be present in Go code. But the question mark operator isn’t limited to Results: try_trait is a feature that allows Rust programmers to use the ? operator on any types implementing the Try trait, so that they can use ? for any short-circuiting logic.

Short-circuiting

There’s a whole suite of higher-order functions under Iterator that demonstrate short-circuiting logic: try_find, try_fold, and try_for_each. Each of these try-flavored methods takes in a function that returns something of the type Try and applies that function to the elements in the iterator, stopping once the function fails. For example, here’s how try_for_each behaves:

let mut it = [1, 2, -3, 4, -5].iter();
let result = it.try_for_each(|&i| {
    if i < 0 {
        Err(format!("Value {} is negative.", i))
    } else {
        Ok(())
    }
});
assert_eq!(result, Err("Value -3 is negative.".to_string()));
assert_eq!(it.next(), Some(&4));

try_for_each attempts to apply the closure on each element. As long as the closure returns Ok, iteration continues as normal, but upon reaching an Err, the iteration short-circuits and returns that result. As we can see in the example, the remaining elements are still available after the call short-ciruits.

Result is probably the most common return type used when applying these iterator functions. But the type signature of the closure passed into try_for_each is FnMut(Self::Item) -> impl Try<Ok = ()>, and it turns out Option implements Try as well. We can modify the closure in the above example to return an Option instead, and it behaves in a similar way, with Some acting as an analogue for Ok and None for Err:

let mut it = [1, 2, -3, 4, -5].iter();
let option = it.try_for_each(|&i| {
    if i < 0 {
        None
    } else {
        Some(())
    }
});
assert_eq!(option, None);
assert_eq!(it.next(), Some(&4));

Since Option also implements Try, we can use ? to write code that propagates Options instead of Results:

fn bar() -> Option<i32> {
    Some(3)
}
fn baz() -> Option<i32> {
    None
}
fn foo() -> Option<i32> {
    let i = bar()?;
    let j = baz()?; // Early return here.
    i + j
}

Tic-tac-toe

Rust’s algebraic data types make the language a great choice for describing board game state. If we’re implementing tic-tac-toe, we might come up with the following data structures to encode the state of the board:

enum Player {
    X,
    O,
}

struct State {
    board: [Option<Player>; 9],
}

And then to manage the progression of the game, we can create a wrapper around the board to manage game state as it transitions between different phases.

enum Phase {
    InProgress(State),
    Win(State, Player),
    Tie(State),
}

fn transition(mut state: State, input: &Input) -> Phase {
    state.apply_input(input);
    if let Some(winner) = state.winner() {
        Phase::Win(state, winner)
    } else if state.is_tie() {
        Phase::Tie(state)
    } else {
        Phase::InProgress(state)
    }
}

With these data structures in place, we can write a really pretty game loop that looks something along the lines of

let mut phase = Phase::InProgress(State::initial());
while let Phase::InProgress(state) = phase {
    let input = read_input();
    phase = transition(state, &input);
}
println!("Game over!\n{}", phase);

One thing we notice about our implementation is that there’s a lot of short-circuiting logic: we short-circuit in transition if we notice the game is over (someone has won or there’s a tie), and the main loop breaks once the game is no longer in progress. This sounds like the perfect use case for try_trait.

If we draw analogues between Phase and Result, we see that Phase::InProgress and Result::Ok are both values that indicate whether execution should continue, while Phase::Win, Phase::Tie, and Result::Err indicate that execution should halt. The target behavior we’d want looks like this:

fn test_result() -> Result<u32, &'static str> {
    Ok(4)?;
    Err("This value is returned.")?;
    Ok(3)
}
fn test_phase() -> Phase {
    Phase::InProgress(State { ... })?;
    Phase::Win(State { ... }, Player::X)?; // This value is returned.
    Phase::Tie(State { ... })
}

With this in mind, we can implement Try for Phase like so:

Implementing Try essentially amounts to writing converters between your type and Result.

impl Try for Phase {
    type Ok = State;
    type Error = (State, Option<Player>);

    fn into_result(self) -> Result<Self::Ok, Self::Error> {
        match self {
            Phase::InProgress(state) => Ok(state),
            Phase::Win(state, winner) => Err((state, Some(winner))),
            Phase::Tie(state) => Err((state, None)),
        }
    }

    // Indicate short-circuiting behavior.
    fn from_error((state, player): Self::Error) -> Self {
        match player {
            None => Phase::Tie(state),
            Some(winner) => Phase::Win(state, winner),
        }
    }

    // Indicate resuming behavior.
    fn from_ok(v: Self::Ok) -> Self {
        Phase::InProgress(v)
    }
}

This is what ? now desugars into.

Now that we’ve enabled the ? operator on Phase, we can go back and rewrite our transition logic:

fn check_win(state: State) -> Phase {
    if let Some(winner) = state.winner() {
        Phase::Win(state, winner)
    } else {
        Phase::InProgress(state)
    }
}

fn check_tie(state: State) -> Phase {
    if state.is_tie() {
        Phase::Tie(state)
    } else {
        Phase::InProgress(state)
    }
}

fn transition(mut state: State, input: &Input) -> Phase {
    state.apply_input(input);
    state = check_win(state)?;
    state = check_tie(state)?;
    Phase::InProgress(state)
}

For tic-tac-toe we only need to check two game-ending conditions, but for complicated games with more game-ending conditions, ? can remove a lot of boilerplate for handling those checks.

Applying try_fold

If we try to generalize playing tic-tac-toe, we’re really just taking an initial state (an empty board) and applying a sequence of modifications to it (placing marks in cells) until we obtain a final state. This procedure is equivalent to taking a fold over the sequence of Inputs, with a seed value of the initial state State::initial() and a combining function transition. Conveniently, we also need to stop execution as soon as we detect that the game is over, which is behavior that try_fold offers. This observation makes writing unit tests for this game a joy:

let inputs = [
    Input { position: 0, player: Player::X, },
    Input { position: 8, player: Player::O, },
    Input { position: 1, player: Player::X, },
    Input { position: 7, player: Player::O, },
    Input { position: 2, player: Player::X, },
    // X has won at this point, so no more inputs are processed.
    Input { position: 6, player: Player::O, },
];
let end = inputs.iter().try_fold(State::initial(), State::transition);
assert_eq!(
    end,
    Phase::Win(
        State {
            board: [
                Some(Player::X), Some(Player::X), Some(Player::X),
                None,            None,            None,
                None,            Some(Player::O), Some(Player::O),
            ]
        }, 
        Player::X
    )
);

A complete implementation of tic-tac-toe using this pattern is available at https://github.com/mingyli/tictactoe-rs.