I often find myself with a dilemma in rust:
- Some function
foodoesn't know up front whether it will need an owned copy of its (potentially expensive to clone) arg, because it depends on the function's internal control flow. Especially when enum variants are involved. - The callers of
foosometimes have a convenient owned value they can donate, but sometimes have only a borrowed reference.
If foo requires owned input, callers who only have a borrowed value are forced to clone it, even tho the function may not actually need ownership after all.
If foo requires borrowed input, callers who could have easily donated an owned value are forced to pass a reference, and the function in turn is forced to clone that reference if it needs ownership after all.
The standard library suffers from this dilemma as well. To give two examples:
- The Ord trait's
cmpmethod requires borrowed args (which is annoying for a caller who has an ownedCopyvalue, e.g.value.cmp(&2)), while the trait'smaxandminmethods require owned args (even tho only one of the two needs to be owned; we just can't guess which one). Similar issues plague all the parent traits (PartialOrd,PartialEq,Eq), and these propagate to sugar syntax like<and!=. - Similarly, the various mathematical operators like Add and Mul take owned args, which only makes sense for
Copytypes. Even from an implementation convenience perspective (mutate and return an owned arg), only one of the args needs to be owned. And for a hypothetical type like an arbitrary precision integer, the output of aMulwill almost always be larger than either input, neither of which is mutated. And again, this awkwardness propagates to the sugar syntax like+and*. - HashMap::entry takes an owned key, but only needs an owned value if the method returns
Entry::VacantAND the user calls an insertion method such as Entry::or_default.
Several standard library concepts dance around this issue without really solving it:
std::borrow::Borrowandstd::convert::AsRefare the opposite of what we need -- they allow a function that always wants a reference to accept a value or a reference from the caller, and the receiver can obtain a reference by calling itsborroworas_refmethod, respectively.std::borrow::Cowexplicitly captures the ability to pass owned or borrowed data, but is very clunky to use and imposes lifetime noise. And it also hides the borrowed vs. owned status behind enum variants, which forces branching that cannot be eliminated with generics, even if the caller always passes either owned or borrowed.std::borrow::ToOwnedis frustratingly close... but itsto_ownedmethod receives&selfand so would force cloning even for the T -> T case.
After playing around a bit, I settled on the following:
/// A zero-cost generalization of [`std::borrow::Cow`].
///
/// Some functions do not know up front whether they will need an owned copy
/// of their input, especially when dealing with enum variants. They must decide
/// whether to require callers to pass borrowed or owned values, which leads to
/// unnecessary cloning.
pub trait IntoOwned<T: Clone> {
/// If true, [`Self::into_owned`] is guaranteed not to clone a borrowed value.
fn is_owned(&self) -> bool;
/// Returns a (possibly cloned) instance of `T`, consuming self.
fn into_owned(self) -> T;
/// Returns a borrowed reference to `T`
fn as_ref(&self) -> &T;
}
An indecisive function can then accept impl IntoOwned<Foo> and chooses whether to call as_ref or into_owned, knowing that into_owned only triggers a clone if the caller passed a borrowed reference. Callers are under no obligation to pre-emptively clone their borrowed reference, knowing that the function will clone it only if needed.
As the playground example shows, the trait allows blanket impl for T, &T, &mut T, Cow<'a, T>, Box<T>, Arc<T>, etc.
One could also require impl Into<T> + Copy if they just want to improve ergonomics for functions that take e.g. integers and whose callers often have to dereference a borrowed reference (i.e. because of a match statement). Virtually all of the math and comparison functions suffer this problem, for example.
This is somewhat related to Ergonomics initiative discussion: Allowing owned values where references are expected, but would not require any language changes. It could even technically be a crate instead of going in the standard library, but that would not allow fixing up the ergonomics of existing standard library functions that suffer this problem.
So the question: Might a trait like this improve ergonomics enough to be worth considering for some future edition of Rust? What explorations would be helpful to answer that question? Did I miss any important details that would either kill this idea or make it even more appealing?
For example, updating library API to allow it would potentially churn several types and classes, but I think it would generally be forward compatible for users. Callers can still pass *x or x.clone() to functions that used to require T, and callers can still pass &x to functions that used to require &T.