Xharlie
Xharlie

Reputation: 2540

how to use `borrow::Borrow<…>` to accept anything that "borrows as" a trait?

The documentation for Rust's core::convert::AsRef trait says:

Borrow has a blanket impl for any T, and can be used to accept either a reference or a value.

It then proceeds to link to the core::borrow::Borrow trait.

Indeed, that can be used to write generic code that can accept a parameter either by value or by reference – it represents the concept of anything than can be borrowed as &T and, since T can be borrowed as &T, this trivial example works perfectly:

fn report_by_either<T: Borrow<i32>>(either: T) {
    let x: i32 = *either.borrow();
    println!("x = {}", x);
}
⋮

report_by_either(5); // x = 5
report_by_either(&6); // x = 6

What if one wishes to use Borrow<…> in more complicated scenarios — concretely: in generic code with generic constraints. Instead of representing the concept of anything that borrows as &T, how can one additionally express the constraint that T implements a trait?

A very simple example occurred to me, recently, when trying to work around the fact that Rust's ranges do not all implement Copy.

Consider this function which accepts anything that can provide RangeBounds<i32>:

fn report_by_value<R: RangeBounds<i32> + Debug>(value: R) {
    println!("range-bounds: `{:?}`", value);
}

This leads to a unsatisfyingly inconsistent experience for the caller:

  1. If they pass some ranges, of the types that implement Copy (e.g. RangeTo, RangeToInclusive, …), they'll be just fine.

    let range = ..100;
    report_by_value(range);
    report_by_value(range);
    report_by_value(range);
    
  2. But, for other ranges (e.g. Range, RangeFrom, …) they'd better call clone() or ownership of the range gets stolen:

    let range_from = 1..;
    report_by_value(range_from.clone());
    report_by_value(range_from.clone());
    
    report_by_value(range_from);
    report_by_value(range_from); // use of moved value: `range_from`
    

One way to avoid this inconsistency is just to accept the range by reference:

fn report_by_reference<R: RangeBounds<i32> + Debug>(reference: &R) {
    println!("range-bounds: `{:?}`", reference);
}

But this also leads to clumsy code at the call-site:

report_by_reference(&(4..));
report_by_reference(&(..5));
report_by_reference(&(6..7));

It seemed like the obvious solution was to use borrow::Borrow:

fn report_by_either<R: RangeBounds<i32> + Debug, T: Borrow<R>>(either: T) {
    println!("range-bounds: `{:?}`", either.borrow());
}

Unfortunately, though, this prompted this question because type inference for the generic types does not work for this.

In this particularly scenario, I do not want my users to have to understand or even be aware of the reasons why the range-types don't implement Copy or the inconsistencies that mean that not all of them don't. I want my users to be able to pass me anything that gives RangeBounds<…>. That said, accepting only range-bounds by reference is not a disasterous compromise in my API in this case – report_by_reference(&(6..7)) is clunky but tolerable.

More generally, however, I think that the concept of anything that borrows as a trait is certainly a common want.

How should I achieve it?

Upvotes: 5

Views: 489

Answers (1)

kmdreko
kmdreko

Reputation: 60052

You're pretty much out of luck here; you have two traits of indirection between 6..7 and your &i32 and the compiler cannot deduce the intermediate type. For example, I can create a type that satisfies the constraints that could be an intermediate type:

#[derive(Debug)]
struct Gobbledygook;

impl RangeBounds<i32> for Gobbledygook {
    fn start_bound(&self) -> Bound<&i32> { todo!() }
    fn end_bound(&self) -> Bound<&i32> { todo!() }
}

impl Borrow<Gobbledygook> for Range<i32> {
    fn borrow(&self) -> &Gobbledygook { todo!() }
}

fn report_by_either<R: RangeBounds<i32> + Debug, T: Borrow<R>>(either: T) {
    println!("range-bounds: `{:?}`", either.borrow());
}

fn main() {
    report_by_either::<Gobbledygook, _>(0..7);
}

If the explicit type parameter were allowed to be omitted, which implementation should the compiler use? Range<i32>? Or my Gobbledygook type? Why? The compiler is not going to assume one way or the other.

So if you have T: Trait<U>, U: Borrow<V> or T: Borrow<U>, U: Trait<V> you'll always have to specify the intermediate type. Though if Trait used an associated type instead of a generic type parameter, then the intermediate type could be unambiguously deduced in the former case.

Upvotes: 2

Related Questions