Reputation: 2540
The documentation for Rust's core::convert::AsRef
trait says:
…
Borrow
has a blanket impl for anyT
, 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:
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);
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.
Both of the following calls yield an error: "cannot infer type of the type parameter R
declared on the function report_by_either
":
report_by_either(5..); // cannot infer type of the type parameter `R` declared on the function `report_by_either`
report_by_either(&(6..)); // cannot infer type of the type parameter `R` declared on the function `report_by_either`
These turbo-fish-blighted lines do work:
report_by_either::<RangeFrom<i32>, _>(7..);
report_by_either::<RangeFrom<i32>, _>(&(8..));
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
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