I mostly agree, or rather,
// The addition of `?Sized` is the important part
fn bar<P: ?Sized + AsRef<Path>>(_: &P) {}
More on that below.
The practical fix is to dereference.
Written before at least some of your comment edits, and mostly just a big long exploration for the sake of a thought experiment -- nothing useful pertaining to your problems or practical for the future of Rust. Feel free to ignore everything below.
As a mental exercise, it is interesting to imagine how things could have been implemented differently. AsRef
has its blanket implementation for APIs like File::open
, and AsRef<Path>
in particular was a significant motivator. Could we have done something different and gotten AsRef<T> for T
instead of the &
-nesting it overlaps with? [1]
An aside about `Into`:
One interesting note in the RFC is the section Why the reference restrictions?. There we can see that the author explicitly wanted to avoid HRTBs [2] such as:
fn bar<P: ?Sized>(_: &P) where for<'any> &'any P: Into<&'any Path>
And later they say that AsRef
implies Into
. However, the blanket implementation making AsRef
imply Into
was later removed as it conflicts with Into
being reflexive.
This theme of wanting to avoid complex signatures comes up again below. That said, I feel it's a worthy separation of concerns in this case anyway; I'd hate to have to write (&x).into()
and hope I got it right.
Now, if we look at File::open
and friends, we can see that they use the pattern I asserted was an anti-pattern:
pub fn open<P: AsRef<Path>>(path: P) -> Result<File>
Which I guess I should back up:
- Open can't do anything but create the
&Path
so it has no need for ownership (of aPathBuf
, say) as_ref
takes a reference as well, so you're not really losing much by requiring a reference as an input, just...- ...the ability to pass something owned into
open
[which] is something you usually don't want to do- Because then you, the caller, can't use it anymore [3]
- A hint to use
AsRef
would help, but there is none in this case, so the unwary will probably.clone
unnecessarily upon getting a "used after move" error - (Or a hint to just use
&
for that matter, given the&
-nesting support)
And one more pertinent quality:
- If you use this pattern, you need to support some level of
&
-nesting in the implementations, because you can't haveP = Path
(or any other unsized type) -- you needAsRef<Path> for &Path
so you can pass in theP = &Path
.
We can't change those patterns now, because those functions accept owned types directly. Bummer. If we had instead
pub fn open<P: ?Sized + AsRef<Path>>(path: &P) -> Result<File>
We, arguably at least, would not have needed the nested &
implementation on AsRef
. [4] As it turns out though, that impl
was included from the start. And hey, if you read the RFCs closely, they actually did use this ?Sized
and &P
pattern in the Path
reform discussion (though often forgetting to write ?Sized
). So what gives?
it's much more ergonomic to not deal with
?Sized
at all and simply require an argumentP
instead of&P
.This change is aimed at removing unsightly
?Sized
bounds while retaining the same level of usability as before.
So because it's unsightly , we lost the more correct (IMO) implementations forever
. [5]
That said, again, the PR is not what prompted the &
-nesting implementation, so further insight would have been needed to avoid it. And maybe it justifies its existence in other ways I haven't thought of. [6]
Well then, should we add more conversion traits and deprecate all the functions with the anti-pattern? No, the current situation works well enough in practice, flawed though it may be. A change like that would be massively disruptive to the ecosystem.
One more anti-pattern-esque thing I'll note which applies to both of
pub fn open<P: ?Sized + AsRef<Path>>(path: &P) -> Result<File>
pub fn open<P: AsRef<Path>>(path: P) -> Result<File>
They both potentially save you some typing to call over taking a &Path
(no .as_ref()
), but as a result you get a monomorphized version of the entire function for every type used. You can mitigate this by doing a one-line as_ref()
call and then passing off to a non-generic private method that takes &Path
, but I suspect most people copying std
conventions don't do so. [7]
In summary, optimizing for writing is a bane upon programming. [8]
OK -- congrats on making it this far by the way -- I contemplated the above exploration in the context of your AsRef<T> as T
thread after seeing your filed issue, because it reminded me about how the P: AsRef<Path>
pattern is common but sub-optimal. In particular, I wasn't really taking the context of this thread into account; it ended up here by accident. [9]
But as a post-script, would having AsRef<T> for T
solved the supposed problem in this thread? Well, we can't have both of
impl<X: ?Sized> AsRef<X> for X { /* ... */ }
impl<U: ?Sized, T: ?Sized + ToBorrow + AsRef<U>> AsRef<U> for Cow<'_, T> { /* ... */ }
because T
might implement AsRef<Cow<'static, T>>
, in which case they overlap. So without specialization, I don't think it would have really helped you out. Also, apparently, you found some arrangements of traits you do like which relies on the &
-nesting (in a recent comment edit), so that's sort of amusing. It still must lose out on AsRef<T> for Cow<'_, T>
.
Question though -- and again, sorry if this is just me not reading the thread thoroughly -- is there a practical reason you care, or is this all an "ideal design" exercise? I see a lot of types and calls in the example that just don't make a lot of sense to care highly about to me (though they can come up due to metaprogramming sort of easily I suppose). Having a Cow<'_, PathBuf>
is like taking a &PathBuf
instead of a &Path
, or a &String
instead of a &str
, for example.
And particularly, in the context of the anti-pattern, in what situation do you own a Cow
that you're okay throwing away in a call to something that wants AsRef<Path>
? It can happen, but I think it's pretty rare, for the same reasons accepting PathBuf
is a stumbling block -- if you have a (potentially) owned version, you're probably going to need it later. And you seemed okay entertaining the idea of avoiding the anti-pattern.
So if we discard that use-case [10], what are the remaining situations -- those where you don't want to give away the Cow
? I think the top few would be
- You own the
cow
and you're going to have to pass&cow
orcow.as_ref()
anyway so as to not give it away- So no great gain over having to do
&*cow
or the like
- So no great gain over having to do
- Your
Cow
is a field and you can't just passself.cow
either- So similar to the previous situation
- You own
cow_ref: &Cow<'_, T>
- but that should probably be a
&T
instead
- but that should probably be a
- You own
cow_mut: &mut Cow<'_, T>
- but that should probably be a
&mut <T as ToOwned>::Owned
(or&T
) instead
- but that should probably be a
I think this isn't a bigger deal in the ecosystem because it just doesn't come up much, and when it does it's easy to work around. The most frequent requests do seem to be of the AsRef<Path> for Cow<'_, str>
variety specifically. Maybe you're hitting it because you're trying to be super generic?
I use "
&
-nesting" as a synonym for "As
lifts over&
". ↩︎and where clauses more generally ↩︎
unless it's
Copy
↩︎And the "oops I gave away ownership" speed bump would be removed, though you might need to explicitly deref where you don't today. ↩︎
Ergonomics of the
std
writer shouldn't outweigh good design for thestd
consumer. ↩︎So I guess the takes-ownership pothole is the main fallout from that PR, combined with the fact that others (including myself) often copy
std
's patterns, so the anti-pattern spreads. ↩︎Less cheekily, using generics for ergonomics is hard to get right, and also hard to change without breaking something. ↩︎
And I've only skimmed this one, so pardon me if I retread old ground. ↩︎
i.e. let's say you don't use the anti-pattern so you can't take owned values ↩︎