Search code examples
rusterror-handlingside-effects

How can I communicate whether side effects have been successful or not?


With the Rust project I am working on I would like to keep the code as clean as I can but was having issues with side effects - more specifically how to communicate whether they have been successful or not. Assume we have this enum and struct (the struct may contain more members not relevant to my question):

enum Value{
    Mutable(i32),
    Constant(i32),
}

struct SomeStruct {
    // --snip--
    value: Value
}

I would like to have a function to change value:

impl SomeStruct {
    fn change_value(&mut self, new_value: i32) {
        match self.value {
            Value::Mutable(_) => self.value = Value::Mutable(new_value),
            Value::Constant(_) => (), /* meeeep! do not do this :( */
        }
    }
}

I am now unsure how to cleanly handle the case where value is Value::Constant.

From what I've learned in C or C++ you would just return a bool and return true when value was successfully changed or false when it wasn't. This does not feel satisfying as the function signature alone would not make it clear what the bool was for. Also, it would make it optional for the caller of change_value to just ignore the return value and not handle the case where the side effect did not actually happen. I could, of course, adjust the function name to something like try_changing_value but that feels more like a band-aid than actually fixing the issue.

The only alternative I know would be to approach it more "functionally":

fn change_value(some_struct: SomeStruct, new_value: i32) -> Option<SomeStruct> {
    match self.value {
        Value::Mutable(_) => {
            let mut new_struct = /* copy other members */;
            new_struct.value = Value::Mutable(new_value);
            Some(new_struct)
        },
        Value::Constant(_) => None,
    }
}

However, I imagine if SomeStruct is expensive to copy this is not very efficient. Is there another way to do this?

Finally, to give some more context as to how I got here: My actual code is about solving a sudoku and I modelled a sudoku's cell as either having a given (Value::Constant) value or a guessed (Value::Mutable) value. The "given" values are the once you get at the start of the puzzle and the "guessed" values are the once you fill in yourself. This means changing "given" values should not be allowed. If modelling it this way is the actual issue, I would love to hear your suggestions on how to do it differently!


Solution

  • The general pattern to indicate whether something was successful or not is to use Result:

    struct CannotChangeValue;
    
    impl SomeStruct {
        fn change_value(&mut self, new_value: i32) -> Result<(), CannotChangeValue> {
            match self.value {
                Value::Mutable(_) => {
                    self.value = Value::Mutable(new_value);
                    Ok(())
                }
                Value::Constant(_) => Err(CannotChangeValue),
            }
        }
    }
    

    That way the caller can use the existing methods, syntax, and other patterns to decide how to deal with it. Like ignore it, log it, propagate it, do something else, etc. And the compiler will warn that the caller will need to do something with the result (even if that something is to explicitly ignore it).


    If the API is designed to let callers determine exactly how to mutate the value, then you may want to return Option<&mut i32> instead to indicate: "I may or may not have a value that you can mutate, here it is." This also has a wealth of methods and tools available to handle it.

    I think that Result fits your use-case better, but it just depends on the flexibility and level of abstraction that you're after.