Let’s do the full deduction to make sure that it’s fully understood.
The trait implementation in question is the one @quinedot was nice enough to just link above, let’s reproduce the full implementation (source)
impl<T: ?Sized, U: ?Sized> AsRef<U> for &T
where
T: AsRef<U>,
{
fn as_ref(&self) -> &U {
<T as AsRef<U>>::as_ref(*self)
}
}
and let’s also take a look at the function we’re trying to understand (source)
impl File {
pub fn open<P: AsRef<Path>>(path: P) -> io::Result<File> {
OpenOptions::new().read(true).open(path.as_ref())
}
}
what both these things have in common are generic type parameters. In Rust, the variables listed in angle brackets after impl
or after the name of a function in a function definition, (or in a few more places) are generic type parameters. These are universally quantified variables, universal quantification means it reads as “for all”; they also come with preconditions / “constraints” / “bounds” that either come directly after the variable introduction or separately in a where clause.
// +----------+--- generic type parameters (introduced here, further
// | | mentions of ‘T’ or ‘U’ to the right and below here
// | | are referring to the same parameters)
// | |
// | +----- -----+---- bounds
// | | | |
// v vvvvvv v vvvvvv
impl<T: ?Sized, U: ?Sized> AsRef<U> for &T
where
T: AsRef<U>, // <--- additional constraint in a where clause
{
fn as_ref(&self) -> &U {
<T as AsRef<U>>::as_ref(*self)
}
}
impl File {
// +-------------- generic type parameter (introduced here)
// |
// | +--------- trait bound
// | |
// v vvvvvvvvvvv
pub fn open<P: AsRef<Path>>(path: P) -> io::Result<File> {
OpenOptions::new().read(true).open(path.as_ref())
}
}
The placement in a where clause vs. on the argument itself doesn’t make a difference, so
impl<T: ?Sized, U: ?Sized> AsRef<U> for &T
where
T: AsRef<U>,
{
fn as_ref(&self) -> &U {
<T as AsRef<U>>::as_ref(*self)
}
}
is the same as
impl<T, U> AsRef<U> for &T
where
T: ?Sized,
U: ?Sized,
T: AsRef<U>,
{
fn as_ref(&self) -> &U {
<T as AsRef<U>>::as_ref(*self)
}
}
Also let’s ignore the topic of Sized
vs ?Sized
entirely in this post, so we’ll work with
impl<T, U> AsRef<U> for &T
where
T: AsRef<U>,
{
fn as_ref(&self) -> &U {
<T as AsRef<U>>::as_ref(*self)
}
}
impl File {
pub fn open<P: AsRef<Path>>(path: P) -> io::Result<File> {
OpenOptions::new().read(true).open(path.as_ref())
}
}
The way to read these bounds/constraints is as a condition, e.g. with the phrasing “such that”. Then the open
method reads as
For all types P
such that the constraint P: AsRef<Path>
is fulfilled, you get File::open::<P>
as a function with signature fn(P) -> io::Result<File>
We can substitute in in concrete types into this statement. Considering that there’s this implementation
impl AsRef<Path> for String {
fn as_ref(&self) -> &Path {
Path::new(self)
}
}
which has the effect that String: AsRef<Path>
is fulfilled, we can apply the knowlege that “String
is a type P
such as P: AsRef<Path>
” to derive that we have “File::open::String
as a function with signature fn(String) -> io::Result)
”.
Let me repeat in other words: We can consider String
for P
and the type signature of File::open
to confirm that
- since
String
is a type for which the constraint String: AsRef<Path>
is fulfilled, we know we can get/use File::open::<String>
as a function fn(String) -> io::Result<Path>
.
- Also, conveniently, Rust’s compiler offers type inference with the effect that if we have some
s: String
we don’t have to write File::open::<String>(s)
, but instead we can write File::open(s)
; the compiler fills in the missing “::<String>
” for us.
In light of monomorphization, you can think of this generic File::open
definition above a bit like a macro: You can use it to generate lots of (variants of) File::open
functions by substituting appropriate types for P
. These instantiations of File::open
all use a copy of the same function body used to work with different concrete types. The trait bound makes sure that the function body always makes sense: The P: AsRef<Path>
bound makes sure that there is something qualifying as an impl AsRef<Path> for P
that defines a fitting as_ref()
method for the type P
, which is used for the call path.as_ref()
in that function body.
By the way, if you’re calling ‘File::open::<String>
’ at different places in your code, these multiple calls still refer to the same generated function. The compiler makes sure to generate only a single unique ‘File::open::<String>
’ function, and in general there’s only a single unique ‘File::open::<P
’ for each choice of P
that’s actually used somewhere in your code. There’s no unnecessary overhead here; calling generic functions is not as bad as using actual macro expressions where the code is always duplicated for every call. Still other optimizations, in particular inlining may of course result in the code for ‘File::open::<String>
’ to be replicated multiple times in case that function is inlined.
Having seen File::open
as an example of a example of a generic function, let’s look at a generic trait implementation. The example of the impl AsRef<Path> for String
was an example of a concrete (not generic) trait implementation. It defines an as_ref
method for the Self
-type String
with return-type &Path
.
By the way, since there’s lots of different as_ref
-methods for lots of different types with different return types, there’s syntax for uniquely specifying (or “disambiguating”) this very as_ref
method that’s defined in the impl AsRef<Path> for String
from the standard library cited above. Its full name in Rust syntax is “<String as AsRef<Path>>::as_ref
”. Similar to how we can write File::open(…)
instead of File::open<…EXPLICIT TYPE…>(…)
and Rust’s type inference helps us out, implicitly filling in the details, we can also simply write something like path.as_ref()
like in the implementation of File::open
without further specifying the types involved, since they’re clear from context. The method
impl File {
pub fn open<P: AsRef<Path>>(path: P) -> io::Result<File> {
OpenOptions::new().read(true).open(path.as_ref())
}
}
equivalently, but using the more explicit notation, would look like
impl File {
pub fn open<P: AsRef<Path>>(path: P) -> io::Result<File> {
OpenOptions::new().read(true).open(<P as AsRef<Path>>::as_ref(&path))
}
}
……anyways, I was going to talk about generic trait implementations:
Similar to how File::open
defines for every type P
(under certain conditions/constraints) a function File::open::<P>
, Rust also offers generic, i.e. universally (“for all”/“for every”-) quantified trait implementations. The implementation
impl<T, U> AsRef<U> for &T
where
T: AsRef<U>,
{
fn as_ref(&self) -> &U {
<T as AsRef<U>>::as_ref(*self)
}
}
Can be read as
For any two1 types T
and U
such that the constraint T: AsRef<U>
is fulfilled, you get a trait implementation impl AsRef<U> for &T
.
1not necessarily distinct
Such a trait implementation impl AsRef<U> for &T
then has the effect that the constraint &T: AsRef<U>
is fulfilled. In other words, the generic implementation above implies that you know the following implication
If (for two1 types T
and U
) the constraint T: AsRef<U>
is fulfilled, then the constraint &T: AsRef<U>
is also fulfilled.
1not necessarily distinct
I was talking about how/that this relates to why File::open
supports &str
, i.e. why you can use it with P
being &str
, i.e. as a function File::open::<&str>
with the signature fn(&str) -> io::Result<File>
.
In order for us to be able to use a function File::open::<&str>
, we need to make sure that &str: AsRef<Path>
is fulfilled. Why? Remember the explanation of the generic function signature of File::open::<P>
:
For all types P
such that the constraint P: AsRef<Path>
is fulfilled, you get File::open::<P>
as a function with signature fn(P) -> io::Result<File>
which should actually, to be more accurate, be complemented with a second part of explanation, stating as much as
For all types P
you get to use the function fn(P) -> io::Result<File>
only if the constraint P: AsRef<Path>
is fulfilled.
(By the way, the same kind of “only if” second part of the explanation is not something we get for generic trait implementations, since traits are open, i.e. there can be many trait implementations for the same trait; whereas a function like File::open
only has a single definition, so it’s closed: there’s never a way to use File::open
when the constraint P: AsRef<Path>
is not fulfilled.)
Now to check (or “prove” if you like…) that &str: AsRef<Path>
is fulfilled, it suffices to find an appropriate concrete or generic trait implementation. Luckily the generic trait implementation analyzed above can help out. Remember:
If (for two1 types T
and U
) the constraint T: AsRef<U>
is fulfilled, then the constraint &T: AsRef<U>
is also fulfilled.
With appropriate substitution of str
for T
and Path
for U
, we get in particulat
If the constraint str: AsRef<Path>
is fulfilled, then the constraint &str: AsRef<Path>
is also fulfilled.
Great, we’re trying to justify why the constraint &str: AsRef<Path>
is fulfilled, looks like all we need to get there is to make sure std: AsRef<Path>
is fulfilled. This in turn is justified by a concrete trait implementation in the standard library
impl AsRef<Path> for str {
// details omitted……
}
There was a chain of reasoning here to get the the insight that &str
can be passed to File::open
, going through the definition of File::open
which is a generic function, then through a generic trait implementation for &T
to, finally, a concrete trait implementation. These chains of reasoning are very typical of Rust’s trait system and the compiler does this reasoning automatically for you; such a chain of trait implementations usually correlates directly with a chain (or sometimes a whole hierarchy/tree) of function calls being generated, i.e.
- the implementation of
File::open::<&str>
calls <&str as AsRef<Path>>::as_ref
- the implementation of
<&str as AsRef<Path>>::as_ref
calls <str as AsRef<Path>>::as_ref
These kinds of chains can get fairly long, fortunately the compiler can usually optimize away most or all the indirection/overhead introduced by these function calls, so you don’t have to worry that this complex trait system that Rust offers might be coming with much performance impact at run time.