Thanks, I was able to complete my implementation of a generic max() function.
But I was quite surprised by your answer, it took me a few days to understand all the implications (like I said, I've just started learning Rust).
So - this implies that when writing implementations of traits, say some trait called MyTrait, we need to write separate implementations for MyTrait, &MyTrait and &mut MyTrait ? And that's because they're considered to be distinct types, right?
But isn't the type we implement for used just for determining the type of the self
parameter in the function implementations?
If so why not just define (and implement) multiple functions under MyTrait (not for &MyTrait and &mut MyTrait), one taking self
, the other taking &self
, the third taking &mut self
? (I'm probably missing something here).
For example:
Suppose we have the following trait :
trait Doable {
fn doit(self);
}
and we want to implement it for some struct D:
struct D {
}
And we want to handle both these functions:
fn test_doable1<T>(t : T) where T : Doable {
t.doit();
}
fn test_doable2<T>(t : &T) where for<'a> &'a T : Doable { // using the syntax from your answer
t.doit();
}
Given this, we'd have to write:
impl Doable for D {
fn doit(self) { println!("Doable::doit"); }
}
impl<'a> Doable for &'a D {
fn doit(self) { println!("Doable for &D::doit"); }
}
impl<'a> Doable for &'a mut D {
fn doit(self) { println!("Doable for &D::doit"); }
}
Which is the same as what was done for Vec, I suppose - it implements IntoIterator 3 times (as can be seen from its source).
But! If Doable was defined as:
trait Doable {
fn doit(self);
fn doit_ref(&self);
fn doit_ref(&mut self);
}
Then we could only implement Doable once, and users of our interface could choose to call the correct function:
impl Doable for D {
fn doit(self) { println!("Doable::doit"); }
fn doit_ref(&self) { println!("Doable::doit_ref"); }
}
fn test_doable1<T>(t : T) where T : Doable {
t.doit();
}
fn test_doable2<T>(t : &T) where T : Doable {
t.doit_ref();
}
This means that when we want to write generic functions which accept a Doable, we only have to call the right function, instead of specifying those complex-looking generic constraints in each function.
Is there a problem with this which I'm missing? If not, why isn't this method used instead of implementing a trait 3 times?
Now, about that syntax:
fn first1<T, I>(col: &T) -> &I
where for<'a> &'a T: IntoIterator<Item = &'a I> {
...
}
-
You specified the IntoIterator::Item as a separate type I
just so you could connect their lifetimes, right?
-
The need to connect their lifetimes is because that's cannot be inferred by the compiler, and the reason is that it cannot know if how I'm ever going to use IntoIterator::Item inside the function, I have to declare it myself, and this declaration is part of the function's signature, right?
-
This syntax of the part after the where
is what's called higher order trait bounds (HRTB) ? Or is this something else?
-
I don't quite understand why the following version (without HRTB or whatever it's called) doesn't compile:
fn first1_bad<'a, T, I>(col : &'a T) -> &'a I
where T : IntoIterator<Item = I>
{
col.into_iter().next().as_ref().unwrap()
}
This fails with
error: borrowed value does not live long enough
|
25 | col.into_iter().next().as_ref().unwrap()
| ^^^^^^^^^^^^^^^^^^^^^^ does not live long enough
Is this because that 'a
lifetime parameter, which I put in the function type list, is only relevant when I
is directly used, but here next() returns an Option<I>
?
And if so, is that special syntax you used meant to cover this, that is, to make each reference to I
anywhere (in this case inside Option<I>)
to have the same lifetime as T ?
Sorry, that came out too long, but there are too many things in your short answer which I felt I had to understand before going on with learning Rust.
Thanks again for your time