Compiler doesn't understand generics the way I do

pub fn read_directory<T>(dir: T) 
where
    T: AsRef<Path>,
{

later on in body of this fn:

if dir.parent()//but here I'm getting error:
^^^^^^ method not found in `T

How so???
My understanding of the construct is:
dir is a of type T that implements AsRef < Path > . Path has the method "parent", so why the compiler cannot see that the method exists?

because it is not Path but rather AsRef<T> so you should directly get reference first via as_ref

3 Likes

You need to call as_ref() on it to actually get a &Path you can call the method on.

4 Likes

The AsRef trait is nothing special. It just requires T to implement a method fn as_ref(&self) -> &Path, which you have to call if you want to use Path's methods. The only traits that do this operation implicitly are Deref and DerefMut, but you should generally avoid using them as trait bounds.

3 Likes

OK, thanks guys. Clarified, but still, weird... Why there is no auto deref here is beyond me.

Generally Rust prefer explicitness, except few special cases of type coercion Type coercions - The Rust Reference (rust-lang.org)

2 Likes

Yes, but what non-obvious thing could happen here? I can understand being explicit about having to write clone() and to_string(), but here? What are we being explicit about? The dir object is already AsRef so having to say yet again, as_ref on AsRef is not being explicit, it being repetitive without deeper sense.

AsRef is trait, which implementation is never going to be noop without optimizations.
By forcing you to call as_ref() directly it hints you that it is not noop operation, in fact depending on operation it might be something more complicated than just &self

3 Likes

If it was implicit then each time you use dir as a Path, even just converting it to a &Path, will call a method, which can do anything.

2 Likes

What method will it call?

It will have to call dir.as_ref(), otherwise how can you get an &Path?

2 Likes

My point exactly. Why do I have to spell it out? Why cannot be done by compiler?
To expand, having type implementing AsRef interface, it should be obvious to the compiler that in order to access that object as_ref needs to be called. Same with auto deref on smart pointers etc. It is simply inconsistent.

Because AsRef is not Deref. Deref is special, since it is associated with the operator - when x is not a reference or pointer, *x is desugared to *(x.deref()). But for AsRef there's no such operation - it's just a trait like any you can create manually.

5 Likes

I understand it, but it simply doesn't make sense, or rather, it makes one explicitly state what compiler could do and there is no other way to do it as to call as_ref. Pointless repetition in my opinion. Too verbal without much point. Don't get me wrong, I do like the fact that rust prefers being explicit, but there are situations where this is just not being explicit but pointlessly repetitive.

It could be done by the compiler, but the reason is that it would be bad if that was the case. Having multiple hidden function calls is bad, so they should be minimized as much as possible. Deref and DerefMut are an exception because sometimes you really really want this behaviour, but part of the reason this is allowed is because this is limited to them.

In your case the best solution would be doing let dir = dir.as_ref() at the start of your function. You do only one function call, avoiding potential inconsistent results from the multiple .as_ref(), while also not having to repeat yourself. Note though that this isn't a valid solution for the compiler: in the general case you may use an &mut T, which must invalidate the result of the .as_ref() call, so you would end up with even more unpredictable hidden function calls.

4 Likes

If it doesn't make sense to you, it doesn't mean there is no sense. Just get over with it and accept it as fact. If you do not want to call AsRef::as_ref then just accept &Path as parameter rather than generic with AsRef

4 Likes

Fair points, thanks

Oh, and in addition to my previous comment: I think there could be a way the compiler does autoderef with AsRef, but I'm not entirely sure. Deref has an associated type which makes it clear what type it should autoderef into, but AsRef has a generic parameter, so a type may implement multiple AsRefs, which could potentially made the target type ambiguous. For example any type T implements AsRef<T>, so should that be considered? It could create infinitely many autoderef targets. Should str have all the [u8] methods directly available? It implements AsRef<[u8]> after all.

1 Like

In addition to this, as noted previously AsRef<T> could do literally anything. Frankly, I prefer the explicitness of as_ref() over auto-as_ref()-ish behavior from the compiler. It would be extremely frustrating and annoying if the compiler did this automatically but someone implemented AsRef<T> to do something over HTTP, or do something else that is really slow, and then me being unable to figure out what's causing it because the problem isn't in my code and its not obvious (even in a debugger) what I'm doing to make that behavior occur.

2 Likes

Note that most things (as SkiFire13 notes below, only Path and PathBuf) which are T: AsRef<Path> would, in practice, implement T: Deref<Output=Path>, so you're not meaningfully restricting yourself by accepting &Path and passing a reference at the call site. Most of the time it's better to be non-generic, for both improved code clarity and performance. AsRef is more valuable for other types, where there may be no Deref implementation.

Since Deref is called automatically by deref coercions, it is strongly discouraged to implement Deref for anything other than a smart pointer. For example, if you make a wrapper struct Foo(Bar), then it's not a good practice to impl Deref for Foo: that's both unidiomatic and can lead to unexpected conversions. You are, however, welcome to impl AsRef<Bar> for Foo. This has an additional benefit that you can unambiguously impl AsRef<R> for T for several different R's and the same T (whether it is actually a good idea is up to you).