Has anyone encountered use of ControlFlow::Continue where the value is other than ()?

The default type for the second generic argument of std::ops::ControlFlow is (). This makes sense in usual cases since it matches how loops behave in Rust. However, I am curious if anyone has encountered a use case where the second generic argument is other than ().

The reason I am curious is because I am trying to convert this enum to an algebraic effect signature for a language I'm designing and I would like to know the enum's semantics.

I think the intended semantic would be for something like a tree walk or pagination, where it returns the next location to search, or perhaps a state machine, returning the next state to evaluate until it reaches a halting condition.

I've not seen it in the wild to my memory (I vaguely remember some version of winit might have used it at some point?), but the std try_trait_v2 feature does have some mappings, like from Option that uses the "not exceptional/failed" value, so it essentially looks like that same "keep going" semantic I suggested above.

Yes, I have used it for re-entrant processing of items, where it could stop processing in the middle of things and return information about what it needed to be able to continue. Maybe not the most perfect case, but it was nice to use with ?.

I've used the second generic argument to use the enum for simple short-circuiting. In my case, both generic arguments are the same type. In practice, it looks like:

pub fn get_pair_mut<B, C>(&mut self) -> (Option<&mut B>, Option<&mut C>)
where
    B: OptionalMetadataBlock,
    C: OptionalMetadataBlock,
{
    use std::ops::ControlFlow;

    match self
        .blocks
        .iter_mut()
        .try_fold((None, None), |acc, block| match acc {
            // both are None, so try first and then try second
            (first @ None, second @ None) => {
                ControlFlow::Continue(match B::try_from_opt_block_mut(block) {
                    Ok(first) => (Some(first), second),
                    Err(block) => (first, C::try_from_opt_block_mut(block).ok()),
                })
            }
            // only second is None, so only try second
            (first @ Some(_), None) => {
                ControlFlow::Continue((first, C::try_from_opt_block_mut(block).ok()))
            }
            // only first is None, so only try first
            (None, second @ Some(_)) => {
                ControlFlow::Continue((B::try_from_opt_block_mut(block).ok(), second))
            }
            // neither is None, so we're all done
            pair @ (Some(_), Some(_)) => ControlFlow::Break(pair),
        }) {
        ControlFlow::Break(p) | ControlFlow::Continue(p) => p,
    }
}

The semantics of OptionalMetadataBlock aren't all that important for this example. But the end result is a method that does a basic shared borrow across a couple of different blocks and stops as soon as both are found.

So when you call the function that returns ControlFlow, is the return value immediately matched on? Or do you find a use case for the calling function to return the same ControlFlow value?

I had to look up the code again and see so I wasn't misremembering (it has been a while) and it seems like I removed the return values it carried and only used ControlFlow for its semantics. Sorry for the bad example.

The idea was at least to process a number of items, then wait for user input, then continue processing the items. It would have passed the ControlFlow value up a number of calls and matched on it at the top to see if it was done or not.

A function or state machine simulating a generator would return Continue<T> for the yielded values, and Break<U> for the eventual return value (often but not always U = ()).

In a newly designed language it might be a semantically better match if iterators returned something like ControlFlow rather than Option, too.

That's the goal, but instead of an enum it's an algebraic effect signature with two operations: break and continue, corresponding to the Rust enum. In the language I'm designing, there is no built-in loop constructs. Loops are expressed as combinators that take in a thunk that performs the ControlFlow effect which can be thought of as the loop body and handles it accordingly.

A benefit of this being an algebraic effect is that not only loops can break and continue. You said it yourself: iterators and generators are perfect use cases for this abstraction.

ControlFlow<A, B> is a bit like Either<A, B>, except it encapsulates some form of "short-circuit" condition, instead of a general if-else branch as Either<A, B> does.

the most common use of it is through the Try trait, you can think of ControlFlow<B, C> as a weaker or generalized form of Result<C, B>.

note the order is reversed, because with Result, you want to "continue" when it's Ok, but "break" when it's Err.

the documentation of ControlFlow mentioned Iterator::try_for_each(), which doesn't use the Continue variant. but another iterator api try_fold does demonstrate how the Continue variant can be used to model short circuit in a loop, this example is from the documentation of Iterator::try_fold():

let triangular = (1..30).try_fold(0_i8, |prev, x| {
    if let Some(next) = prev.checked_add(x) {
        ControlFlow::Continue(next)
    } else {
        ControlFlow::Break(prev)
    }
});
assert_eq!(triangular, ControlFlow::Break(120));

It's also fundamental to try_trait_v2 which is implemented in terms of ControlFlow

(simplified)

pub trait Try {
...
    // Required methods
...
    fn branch(self) -> ControlFlow<Self::Residual, Self::Output>;
...
}

For example:

const impl<T, E> ops::Try for Result<T, E> {
...
    fn branch(self) -> ControlFlow<Self::Residual, Self::Output> {
        match self {
            Ok(v) => ControlFlow::Continue(v),
            Err(e) => ControlFlow::Break(Err(e)),
        }
    }
...
}

So ControlFlow is used to carry either the output value in a ControlFlow::Continue or the residual in a ControlFlow::Break

It's the same as how Ok(()) is very common, but other things are seen too.

If you look at uses of Iterator::try_fold, you'll see lots of them.

A great many methods on iterators are implemented using it. For example, position is essentially implemented as

iter.try_fold(0, |i, x| { 
    if f(x) { 
        ControlFlow::Break(i)
    } else {
        ControlFlow::Continue(i + 1)
    }
}).break_value()

So the C in the ControlFlow is the loop iteration variable.

Yes. In a ControlFlow method, one is inclined to use ControlFlow<_, Data> for e.g. private supporting functions instead of Result<Data, _> so that ? becomes ergonomic. So when you need some piece of data to continue, but calculating it may result in a break condition (due to an error or w/e).[1]

I was also just now looking at creating a callback based tree traversal, and need some way to distinguish "don't recurse at this level but continue with the rest of the tree" and "exit the tree". Each node happens to know it's children identifiers in a slice, so I'm considering something like

CallBack: Fn(S { children: &[Ids], next_idx: usize }) -> ControlFlow<_, usize>

(or perhaps Option<usize>) to indicate how to continue.


  1. There are conversion methods from ControlFlow to Result now I see, but still nothing the other direction it looks like. I used an extension trait to accomplish that some places. ↩︎