Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Opposite of Borrow trait for Copy types?

I've seen the Borrow trait used to define functions that accept both an owned type or a reference, e.g. T or &T. The borrow() method is then called in the function to obtain &T.

Is there some trait that allows the opposite (i.e. a function that accepts T or &T and obtains T) for Copy types?

E.g. for this example:

use std::borrow::Borrow;

fn foo<T: Borrow<u32>>(value: T) -> u32 {
    *value.borrow()
}

fn main() {
    println!("{}", foo(&5));
    println!("{}", foo(5));
}

This calls borrow() to obtain a reference, which is then immediately dereferenced.

Is there another implementation that just copies the value if T was passed in, and dereferences if &T was given? Or is the above the idiomatic way of writing this sort of thing?

like image 315
Dave Challis Avatar asked Aug 18 '20 09:08

Dave Challis


2 Answers

There is not really an inverse trait for Borrow, because it's not really useful as a bound on functions the same way Borrow is. The reason has to do with ownership.

Why is "inverse Borrow" less useful than Borrow?

Functions that need references

Consider a function that only needs to reference its argument:

fn puts(arg: &str) {
    println!("{}", arg);
}

Accepting String would be silly here, because puts doesn't need to take ownership of the data, but accepting &str means we might sometimes force the caller to keep the data around longer than necessary:

{
    let output = create_some_string();
    output.push_str(some_other_string);
    puts(&output);
    // do some other stuff but never use `output` again
} // `output` isn't dropped until here

The problem being that output isn't needed after it's passed to puts, and the caller knows this, but puts requires a reference, so output has to stay alive until the end of the block. Obviously you can always fix this in the caller by adding more blocks and sometimes a let, but puts can also be made generic to let the caller delegate the responsibility of cleaning up output:

fn puts<T: Borrow<str>>(arg: T) {
    println!("{}", arg.borrow());
}

Accepting T: Borrow for puts gives the caller the flexibility to decide whether to keep the argument around or to move it into the function.¹

Functions that need owned values

Now consider the case of a function that actually needs to take ownership:

struct Wrapper(String);
fn wrap(arg: String) -> Wrapper {
    Wrapper(arg)
}

In this case accepting &str would be silly, because wrap would have to call to_owned() on it. If the caller has a String that it's no longer using, that would needlessly copy the data that could have just been moved into the function. In this case, accepting String is the more flexible option, because it allows the caller to decide whether to make a clone or pass an existing String. Having an "inverse Borrow" trait would not add any flexibility that arg: String does not already provide.

But String isn't always the most ergonomic argument, because there are several different kinds of string: &str, Cow<str>, Box<str>... We can make wrap a little more ergonomic by saying it accepts anything that can be converted into a String.

fn wrap<T: Into<String>>(arg: T) -> Wrapper {
    Wrapper(arg.into())
}

This means you can call it like wrap("hello, world") without having to call .to_owned() on the literal. Which is not really a flexibility win -- the caller can always call .into() instead without loss of generality -- but it is an ergonomic win.

What about Copy types?

Now, you asked about Copy types. For the most part the arguments above still apply. If you're writing a function that, like puts, only needs a &A, using T: Borrow<A> might be more flexible for the caller; for a function like wrap that needs the whole A, it's more flexible to just accept A. But for Copy types the ergonomic advantage of accepting T: Into<A> is much less clear-cut.

  • For integer types, because generics mess with type inference, using them usually makes it less ergonomic to use literals; you may end up having to explicitly annotate the types.
  • Since &u32 doesn't implement Into<u32>, that particular trick wouldn't work here anyway.
  • Since Copy types are readily available as owned values, it's less common to use them by reference in the first place.
  • Finally, turning a &A into an A when A: Copy is as simple as just adding *; being able to skip that step is probably not a compelling enough win to counterbalance the added complexity of using generics in most cases.

In conclusion, foo should almost certainly just accept value: u32 and let the caller decide how to get that value.

See also

  • Is it more conventional to pass-by-value or pass-by-reference when the method needs ownership of the value?

¹ For this particular function you'd probably want AsRef<str>, because you're not relying on the extra guarantees of Borrow, and the fact that all T implements Borrow<T> isn't usually relevant for unsized types such as str. But that is beside the point.

like image 194
trent Avatar answered Sep 22 '22 03:09

trent


With the function you have you can only use a u32 or a type that can be borrowed as u32.

You can make your function more generic by using a second template argument.

fn foo<T: Copy, N: Borrow<T>>(value: N) -> T {
    *value.borrow()
}

This is however only a partial solution as it will require type annotations in some cases to work correctly.

For example, it works out of the box with usize:

let v = 0usize;
println!("{}", foo(v));

There is no problem here for the compiler to guess that foo(v) is a usize.

However, if you try foo(&v), the compiler will complain that it cannot find the right output type T because &T could implement several Borrow traits for different types. You need to explicitly specify which one you want to use as output.

let output: usize = foo(&v);
like image 23
Sunreef Avatar answered Sep 24 '22 03:09

Sunreef