Transforming Variable via &mut vs. owned Parameter

Hi, I'm currently working out how to generally design APIs where a value is transformed, but that operation may fail.

The two ways I'm aware of are:

fn main() -> Result<(), ()> {
    let mut value = "Foo".to_string();
    println!("{}", value);
    
    transform_mut_ref(&mut value)?;
    println!("{}", value);
    
    value = transform_owned(value)?;
    println!("{}", value);
    
    Ok(())
}

fn transform_mut_ref(value: &mut String) -> Result<(), ()> {
    *value = "Bar".to_string();
    Ok(())
}

fn transform_owned(_value: String) -> Result<String, ()> {
    Ok("Baz".to_string())
}

I like the second style, because there's one path for information to flow. Especially in unit tests, there's just one result-value to do assertions on. And as a caller, you have a guarantee that your value wasn't touched, if the operation fails.

However, when the operation does fail, that means the ownership remains in transform_owned() and there's no way to recover from the Err which doesn't involve exiting the scope immediately (via ? or return).

I guess, this would be possible, if transform_owned() returned a Result<String, String>, handing back the original variable, if it fails. But that makes error-handling awkward, as you can't easily wrap that with additional error information and have that handled by ? or similar.

Am I missing something or does everyone just use &mut for these reasons?

That's usually the idiomatic way. You can even make it not String, but TransformError { context: ContextType, original: String }, i.e. combine original value and the error itself into the custom struct.

1 Like

Two examples from std:

Both interfaces are slightly different: Arc::try_unwrap doesn't use a custom error type but simply returns Err(self) on failure, while CString::into_string returns a custom error type.

I guess it depends on the use case what's best to do.

Yet another example is impl TryFrom<Vec<T>> for [T; N], with Error = Vec<T>.

1 Like

Also from a general perspective, TryFrom doesn't put any bounds on its associated Error type (in particular, it doesn't require the "error" to implement std::error::Error).

1 Like

Thanks for the responses, but I think, I'm making a noob mistake.

Having a function require ownership is bad in most situations, because it requires that the caller holds that ownership, when they might have gotten the variable via a mutable borrow themselves.
It means our function can be called in less situations, because we require more (ownership) than is necessary to do the job (mutable borrow).

The examples posted by @jbe work, because those are situations where a transformation is applied directly to a data type and the data type is what holds the ownership to begin with.

So, transform_mut_ref() is what you want in most situations.

I would generally agree and say that ownership should only be required if

  • the value needs to be consumed at least in some cases (or rather: where it's either needed or efficient to consume it), or
  • the type is "mutated" (by consuming a value of one type and returning a value of a different type) (which, for example, happens with the state builder pattern),
  • and maybe also for the builder pattern in general, where you want to consume the mutated builder in the end and like to use chaining syntax.

Though I would like to add that there exists a crate (take_mut) which allows obtaining ownership in safe Rust even if you only have a mutable borrow:

#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

fn mirror(p: Point) -> Point {
    // if a panic happens here, `take_mut::take` will abort the while program
    Point { x: -p.x, y: p.y }
}

fn scale(p: &mut Point) {
    p.x *= 2;
    p.y *= 2;
}

fn foo(p: &mut Point) {
    take_mut::take(p, mirror);
    scale(p);
}

fn main() {
    let mut p = Point { x: 10, y: 10 };
    foo(&mut p);
    println!("{p:?}");
}

(no Playground as take_mut is not available on Playground)

Output:

Point { x: -20, y: 20 }

However, using take_mut::take comes at the price of aborting the program in case of a panic. There is a recoverable version take_mut::take_or_recover which allows recovering from a panic, but it requires providing a closure which then provides a replacement value. In most cases using take_mut won't be idiomatic, I guess, but I wanted to mention it nonetheless.

1 Like

No, this is not unconditionally true as-is. It's fine to require ownership in many situations, but you have to know why. It's not like "ownership bad, reference good". These concepts exist in the language because both borrowed and owned types/values serve useful purposes.

For example, constructors and conversion functions (e.g. From and TryFrom) usually require ownership. Also, many generic functions shouldn't require a reference, because a declaration like

fn foo<T>(value: &mut T)

only works with references, whereas

fn foo<T>(value: T)

works with any type. So it's clearly a lot more nuanced than a blanket "you shouldn't require ownership".

Re. take_mut, there’s also replace_with - Rust which has supposedly a small performance benefit from using destructors instead of catch_unwind, and it has an API that makes it harder to accidentally cause aborts by making the recovering version the one with the shorter name, and calling out the possibility of aborts in the function name of the aborting-on-panic version.


Also, for values with cheap default values, such as String, using std::mem::take to get ownership and writing back the result afterwards should also be reasonably efficient, without any extra crates needed (and its panicking behavior would be to leave the empty string in place).

2 Likes

I was searching for something like take_mut for quite a while, because I figured that must be possible. That does still look useful, but I'm glad, I didn't find it when I had that wrong assumption...

One occasion where you only have a mutable reference but need to move out is when you write a Drop::drop implementation. That is because Drop::drop obtains a &mut self reference, yet sometimes might want to move out of certain fields.

type SomeResource = String;

fn foo(s: SomeResource) {
    println!("Cleaning up {s}");
}

struct X {
    resource: SomeResource,
}

impl Drop for X {
    fn drop(&mut self) {
        foo(self.resource); // won't work
        //foo(std::mem::take(&mut self.resource)); // but we can do this
    }
}

fn main() {
    drop(X {resource: "some resource".into()});
}

(Playground)

However, std::mem::take won't work if the type you want to move out doesn't implement Default. For example:

#[derive(Debug)]
struct SomeOtherResource;

fn foo(s: SomeOtherResource) {
    println!("Cleaning up {s:?}");
}

struct X {
    resource: SomeOtherResource,
}

impl Drop for X {
    fn drop(&mut self) {
        //foo(self.resource); // won't work
        foo(std::mem::take(&mut self.resource)); // won't work either here
    }
}

(Playground)

In those cases, I usually wrap the resource in an Option:

struct X {
    resource: Option<SomeOtherResource>,
}

impl X {
    fn uses_resource(&mut self) {
        // going through `Option` here comes with some runtime overhead
        let _resource: &mut SomeOtherResource = self.resource.as_mut().unwrap();
    }
}

impl Drop for X {
    fn drop(&mut self) {
        foo(self.resource.take().unwrap()); // this is `Option::take` now
    }
}

(Playground)

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.