Final default for chain of potentially failing operations

I am trying to improve the following function in my computer game:

fn is_complete_move_to(fabric: &Fabric, action: &MoveToAction) -> Option<bool> {
    if fabric.get::<Pathing>(action.actor)?.is_unreachable {
        Some(true)
    } else {
        Some(
            Self::entity_target_distance(fabric, action.actor, &action.target)?
                <= action.goal_distance,
        )
    }
}

which I call like this: Self::is_complete_move_to(fabric, a).unwrap_or(true).

I think the function should just return bool directly as I always want failure / None to mean that the action is completed (=true). Later I might introduce an enum for running/success/failure but for now I just want to know if the action terminated so that my actor does not get stuck.

I am struggeling to refactor this into a single function considering the potentially failing helper functions fabric.get and Self::entity_target_distance. Any advice how to do this better?

How do you like this solution with if let statements:

fn is_complete_move_to(fabric: &Fabric, action: &MoveToAction) -> bool {
    if let Some(f) = fabric.get::<Pathing>(action.actor) {
        f.is_unreachable
    } else if let Some(d) = Self::entity_target_distance(fabric, action.actor, &action.target) {
        d <= action.goal_distance
    } else {
        true
    }
}

Thanks this looks already much better! It is not equivalent though.. If is_unreachable is false then I need to execute the entity_target_distance check.

My bad, let's try let-else statements this time :smile::

fn is_complete_move_to(fabric: &Fabric, action: &MoveToAction) -> bool {
    let Some(f) = fabric.get::<Pathing>(action.actor) else {
        return true;
    };
    
    if f.is_unreachable {
        return true;
    }
    
    let Some(d) = Self::entity_target_distance(fabric, action.actor, &action.target) else {
        return true;
    };
    
    d <= action.goal_distance
}

Note that you need a compiler version of at least 1.65, which was when let-else statements were stabilized.

1 Like

Thank you that looks interesting!

I have a bunch of code like this which calls functions which may fail (return Option) because an actor might not have certain properties (or recently lost them). I like ? because it lets me write code as if nothing would fail and then deal with the failure case once at the end.

But I guess ? only works with functions, and not with local scopes, or? So something like is not possible in Rust?

fn foo(...) -> bool {
  {
    if some()?.more()?.endless()? {
      Some(true)
    } else {
      Some(another()?.again()? < 31)
    }
  }.unwrap_or(true)
}

Making ? work in local scopes is the purpose of the unstable try_blocks feature, which adds a try { ... } block that catches any errors from ? expressions. There are a few different technicalities that have caused it to be delayed for quite a while; a catch { ... } block was originally proposed at the same time as the ? operator in 2014.

2 Likes

A way to partially [1] emulate try blocks is with an inline closure...

    (|| {
        some()?.more()?.endless()?.then(|| true)
    })().unwrap_or(true)

...but yeah, not pretty.


  1. No Ok wrapping ↩ī¸Ž

This seems perfectly expressible as-is via existing combinators: Playground

fn is_complete_move_to(fabric: &Fabric, action: &MoveToAction) -> bool {
    fabric.get::<Pathing>(action.actor)    
        .and_then(|p| {
            let flag = p.is_unreachable ||
                Self::entity_target_distance(fabric, action.actor, &action.target)?
                    <= action.goal_distance;

            Some(flag)
        })
        .unwrap_or(true)
}
2 Likes