I was missing that part, even though I cited it in my OP. So the documentation does state that AsRef isn't reflexive (not in the overview in std::convert though, and it does so in the context of Borrow only).
Maybe it could be said more explicitly? Something like:
(example of how it could be phrased)
Trait std::convert::AsRef
Reflexivity
Ideally,
AsRefwould be reflexive, that is there is animpl<T: ?Sized> AsRef<T> for T, withas_refsimply returning&self. Such a blanket implementation is currently not provided due to technical restrictions of Rust's type system (it would be overlapping with another existing blanket implementation for&TwhereT: AsRef<U>which allowsAsRefto auto-dereference, see "Generic Implementations" above).A trivial implementation of
AsRef<T> for Tmust be added explicitly for a particular typeTwhere needed or desired. Note, however, that not all types fromstdcontain such an implementation, and those cannot be added by crates due to orphan rules.Therefore, writing
.as_ref()cannot be used generally to go from a type to its reference, e.g. the following code does not compile:let i: i32 = 0; let r: &i32 = i.as_ref(); // let r: &i32 = &i; must be used instead
Maybe that's a bit easier to understand for newcomers. Particularly, it would explain that AsRef isn't lacking reflexivity for semantic but only for technical reasons (P.S.: and for backwards-compatibility, as I try to show further below).
Explaining this might help to reduce confusion on this subject for non-newcomers as well.
There are several methods in std which take a P: AsRef<Path> (which might or might not be considered to be over-generic). I would say it makes it easy to invoke them, but not sure if that has been a good design decision.
Getting back to some of the breaking examples in my OP, it should be said that circumventing these problems in practice isn't so difficult. We can do:
use std::borrow::Cow;
use std::ffi::OsStr;
use std::fs::File;
fn main() {
let _: Result<File, _> = File::open("nonexistent");
let _: Result<File, _> = File::open(&Cow::Borrowed(OsStr::new("nonexistent")));
// This fails to compile:
// let _: Result<File, _> = File::open(&Cow::Borrowed("nonexistent"));
// If we have a reference to a `Cow`, we must use `&**`:
let _: Result<File, _> = File::open(&**(&Cow::Borrowed("nonexistent")));
}
Just like you said, using &** (or &* if the Cow is owned and may be consumed). Not nice, but doable.
But these practical issues aside, I wonder if the use of .as_ref() in the following is (or rather: should be) considered idiomatic:
use std::borrow::Cow;
use std::fs::File;
fn main() {
let cow: Cow<_> = Cow::Borrowed("nonexistent");
let ref_to_cow: &Cow<_> = &cow;
let _: Result<File, _> = File::open(&**ref_to_cow);
let _: Result<File, _> = File::open(ref_to_cow.as_ref());
}
This works because of:
#[stable(feature = "rust1", since = "1.0.0")]
impl<T: ?Sized + ToOwned> AsRef<T> for Cow<'_, T> {
fn as_ref(&self) -> &T {
self
}
}
(source)
I would like to state the hypothesis that:
- while this implementation exists (and cannot be removed without breaking code), it should not have been added in the first place,
- using
ref_to_cow.as_ref()in the above Playground should be avoided in idiomatic code (hypothesis 4).
Moreover, I believe that existing code which uses .as_ref() in that way makes it impossible to solve the outlined problems by solely adding two blanket implementations (AsRef<T> for T and AsRef<U> for T where T::Target: AsRef<U>, e.g. with negative bounds) in the future.
This can be demonstrated by using my_as_ref as defined in my above post:
fn takes_path(_: impl MyAsRef<Path>) {}
fn main() {
let cow: Cow<_> = Cow::Borrowed("nonexistent");
let ref_to_cow: &Cow<_> = &cow;
let _ = takes_path(&**ref_to_cow);
let _ = takes_path(ref_to_cow.my_as_ref());
}
(Note that the first (reflexive) blanket implementation cannot provided for technical reasons in this demonstration but only a concrete implementation for str.)
Here type inference will fail due to multiple implementations of MyAsRef<_> for str:
Compiling playground v0.0.1 (/playground)
error[E0282]: type annotations needed
--> src/main.rs:78:35
|
78 | let _ = takes_path(ref_to_cow.my_as_ref());
| -----------^^^^^^^^^--
| | |
| | cannot infer type for type parameter `T` declared on the trait `MyAsRef`
| this method call resolves to `&T`
error[E0283]: type annotations needed
--> src/main.rs:78:35
|
78 | let _ = takes_path(ref_to_cow.my_as_ref());
| -----------^^^^^^^^^--
| | |
| | cannot infer type for type parameter `U`
| this method call resolves to `&T`
|
note: multiple `impl`s satisfying `str: MyAsRef<_>` found
--> src/main.rs:51:1
|
51 | impl MyAsRef<str> for str {
| ^^^^^^^^^^^^^^^^^^^^^^^^^
...
66 | impl MyAsRef<Path> for str {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
note: required because of the requirements on the impl of `MyAsRef<_>` for `Cow<'_, str>`
--> src/main.rs:38:12
|
38 | impl<T, U> MyAsRef<U> for T
| ^^^^^^^^^^ ^
help: use the fully qualified path for the potential candidates
|
78 | let _ = takes_path(<str as MyAsRef<Path>>::my_as_ref(&ref_to_cow));
| +++++++++++++++++++++++++++++++++++ ~
78 | let _ = takes_path(<str as MyAsRef<str>>::my_as_ref(&ref_to_cow));
| ++++++++++++++++++++++++++++++++++ ~
Some errors have detailed explanations: E0282, E0283.
For more information about an error, try `rustc --explain E0282`.
error: could not compile `playground` due to 2 previous errors
Ironically just writing takes_path(ref_to_cow) would work! (Playground)
While these considerations are all very hypothetical and ignore that std doesn't do all these things, it might still be worth noting, because
- being able to provide two blanket implementations for
AsRefin the future would not automatically solve all the outlined problems but providing such a second blanket implementation forAsRefmight instead induce more problems with existing code, - third party crates may go a different way than
std.
Regarding the first point, I believe that the problem really cannot be solved without starting over with an entirely new AsRef (likely not possible at this point, but I still hope that somehow the Edition mechanism could be used in future to overcome bad design decisions of the past).
The latter point brings me back to the question:
So what to do in external crates?
- Use
std's practice regarding smart-pointers?- Be consistent with
std's implementation ofAsReffor references?Maybe this question can only be answered individually for each use case? Or perhaps there is no satisfactory answer at all.
Moreover, should .as_ref() be avoided in teaching material in cases where it's used to dereference (edit: generic(?)) smart-pointers?