Links: AsRef, Borrow, BorrowMut.
Those docs have plenty of text. I've read it dozens of times and felt "uhhh, what" every single time. Well, the text sort of makes sense, but, in the end, it doesn't help me in deciding between AsRef and Borrow. So I just had to dig myself through the APIs and try figuring the mystery by myself. :)
Here are some notes I've collected afterwards. I wonder if we could get at least some of the following stuff into the official docs...
| Conversion | Using Borrow | Using AsRef |
|-------------------|----------------|---------------|
| &T -> &T | borrows | (*) |
| &Vec<T> -> &[T] | borrows | borrows |
| &String -> &str | borrows | borrows |
| &str -> &Path | | converts |
| &Path -> &OsStr | | converts |
| &OsStr -> &Path | | converts |
| &Path -> &Path | borrows | borrows |
(*) should be 'borrows', but cannot be implemented yet due to coherence issues (I believe?)
Key takeaways:
Borrow is simple and strict. The hash of the borrowed reference must stay the same.AsRef converts to a wider range of different types. The hash of the new reference is allowed to change.We want to implement a function that creates a directory. It must accept both &str and &Path as the argument. Which signature are we looking for?
fn mkdir<P: AsRef<Path>>(path: P);
fn mkdir<P: Borrow<Path>>(path: P);
Answer: In order to go from &str to &Path, we have to create a value of different type Path (&str is more primitive - it cannot be borrowed as Path). Since AsRef can borrow and convert, it is the correct option here.
We want to check whether a value exists in a HashSet. Even if we have a set of type HashSet<String>, we'd like to be able to just do s.contains("foo") (passing a &str). Which one of the four method signatures is the right one?
impl<T: Eq + Hash> HashSet<T> {
fn contains<Q: Hash + Eq>(&self, value: &Q) -> bool where T: AsRef<Q>;
fn contains<Q: Hash + Eq>(&self, value: &Q) -> bool where Q: AsRef<T>;
fn contains<Q: Hash + Eq>(&self, value: &Q) -> bool where T: Borrow<Q>;
fn contains<Q: Hash + Eq>(&self, value: &Q) -> bool where Q: Borrow<T>;
}
Answer: We don't want to convert between totally different types, so the hash and structural equality of the value must be preserved after reference conversion. In other words, we only want simple borrowing. Conversion to a different type might potentially change the hash and is thus out of the question.
So Borrow is the right one here. But is it T: Borrow<Q> or Q: Borrow<T>? Well, if T is String and we're passing a &str to the method, we want to borrow T as Q. So the right bound is T: Borrow<Q>.
I'm willing to work on this soon, updating both the book and the rustdoc. I think that the main thing that isn't conveyed right now is the fact that Borrow requires that the data be functionally the same (providing the same trait impls) whereas AsRef reinterprets the data differently (e.g. str -> Path).
I’ve been planning to write some text for the documentation of AsRef and Borrow. In particular, I think what is currently lacking is a one-sentence summary of the purpose of the two. This seems important to me because if one reads a type implementing a trait as ‘being something,’ Borrow is backwards: T: Borrow<U> can intuitively be read as T is a borrow of U when in fact it is the other way around. This had me seriously confused when looking at the signature of HashMap::get().
So, for Borrow my proposal would be something along the line of: ‘If a type implements Borrow<Borrowed>, it signals that the type Borrowed acts as a kind of pointer to the type.’ (Pointer may not be the right word here, but I feel that using ‘borrow’ to explain Borrow is a bit weird.)
Conversely, perhaps: ‘By implementing AsRef<T>, a type signals that it can provide a view of itself as T.’ (That one probably needs some work.)
@clarcharr, are you still planning on working on this or do you mind if I give it a shot?
I completely forgot! Go ahead and I can still offer to review it after. :)
This is the best documentation on these conversions so far, I've found. As someone else is working on the docs, at the very least - I'd link to this discussion, on the book.
I continuously get confused between T: Borrow<Q> and Q: Borrow<T>. I think the mental rule to remember is that Borrow is really BorrowAs. Then it all becomes a lot clearer. Of course, we can't actually rename the trait at this point.
A related discussion to this (I think) is what a user should do if they want to take any type that can be borrowed into an &T including T. In that case you must use Borrow, since there is no impl AsRef<T> for T.
@jonhoo, we’ve tried to build this sort of mental model for Borrow in its new documentation. If you haven’t yet, could you perhaps check if it feels more clear?
As for the missing blanket impl AsRef<T> for <T>, I haven’t found that to be all that much of an issue in practice, since in most cases I use AsRef<T> for some concrete T – AsRef<[u8]> and AsRef<Path> come to mind, and those types so far have always had that impl anyway.
You can’t use Borrow<T> in these cases, necessarily, since it comes with additional restrictions. The new documentation has the example of a newtype for a string that compares without considering ASCII case. While that newtype can impl AsRef<str>, it mustn’t have impl Borrow<str> as that would break hash maps and the like.
@partim The new documentation is certainly better! I think the confusion stems mostly from the name of the trait though. When I read T: Borrow<Q>, I instinctively think "I can borrow T from a Q", whereas the reality is the other way around. Even reading the example it seems pretty weird: my intuition would be that we need Q to satisfy some bound for it to be usable as a key, whereas it's phrased as a bound on K. It's almost like Borrow is "backwards"?
I think one thing the documentation could use is an explicit "which should I implement and why" section. It's currently mixed in with the CaseInsensitiveString example, but I think it could use being called out.
I also found the statement "For instance, a Box<T> can be borrowed as T while a String can be borrowed as str" somewhat weird. I understand that it's technically correct, but I'd probably be more inclined to say that Box<T> can be borrowed as &T (and similarly &str).
For the lack of a T: Borrow<T>, I think this is where some libraries use Q: Into<Cow<T>>, and maybe that's worth referencing here too?
I've taken a stab at this at https://github.com/rust-lang/rust/pull/59663. It seems like explicitly mentioning that every impl of Borrow must maintain Eq, Ord, Hash would do the trick.
Most helpful comment
This is
the bestdocumentation on these conversions so far, I've found. As someone else is working on the docs, at the very least - I'd link to this discussion, on the book.