So, clearly it'd be nice if we can avoid it, I'm not sure if we can though. But actually the question you raise is deeper than I originally thought. In particular, I think we need to do a bit more thought about what a where-clause written on an ATC means -- specifically, when will it be verified and so forth? In other words, who gets to rely on what. =)
Let me start by explaining the situation with functions in more depth. In that case, we require that fn callers verify that the types of arguments are well-formed, which then allows the callee to just assume this to be the case. This in turn means that the fn body can assume Self: 'a
-- in a sense, it's an implicit where-clause. (*)
We actually pull a similar trick with associated types already. In particular, we say that if you have a projection like <T as Foo>::Bar
, then in order to normalize this into a real type, you must first show that the type T
is well-formed, along with any other trait parameters (the input types, in other words). This, in turn, means that the impl can assume that the self type is well-formed and so forth.
This is why you can write an impl like this:
impl<'a> Iterator for &'a [T] {
type Item = &'a T;
// ^^^^^ for this to be WF, we must know that T: 'a
}
The problem here is that, for &'a T
to be well-formed, we must know that T: 'a
. However, because we can assume that &'a [T]
is well-formed, that implies that [T]: 'a
which in turn implies that T: 'a
, so we're all set.
So why can't we do a similar trick with the associated type constructor case? Consider this impl:
impl<T> Iterable for [T] {
type Iterator<'a> = slice::Iter<'a, T>
// ^^^^^^^^^^^^^^^^^^
// again, WF only if `T: 'a`
}
The problem is, the types on the impl in this case don't mention 'a
! Knowing that [T]
is well-formed doesn't really tell us anything interesting.
So, how then is the impl supposed to be able to prove that T: 'a
? There are basically two choices:
We can declare the constraint in the trait (as I did). My intention in doing so was to say that it was an extra condition which must be proven in order to show that a projection is WF. In other words, if you have a type like <T0 as Iterable>::Iterator<'a>
, you must of course show that T0
is well-formed (as ever). But, because of the where-clause, you must also show that T0: 'a
, which would not ordinarily be required. However, I think this is not quite how we have been thinking of where-clauses placed on ATCs up till now. Let me come back to this in a sec.
Another option would be to impose such a constraint universally. For example, we might say that whenever you have <P[0] as Foo<P[1]..P[n]>::Bar<'a>
, then P[i]: 'a
must hold for all i
, regardless of the particulars of Foo
and Bar
. This is what we do with the basic WF rules: we say that every input to the projection must be well-formed, even if it wouldn't appear in the output in the end; but this is no burden because WF types are something we always want. This would mean that nobody has to declare Self: 'a
, because it is always required. Another way of saying this is that we assume that whenever you have an ATC like type Iter<'a>
, we assume that you are somehow creating a reference to the entire self
value (not just some parts of it). Or at least we impose requirements that are strict enough for you to do so, whether or not that is what you wanted.
Imposing this constraint universally might actually be the right way to go. I am worried it will be too strict, but I confess I am having trouble coming up with a convincing counter example. Here is an artificial one. Imagine I have a tree of things that can be hashed using the hasher H
and which can also be iterated.
trait EncodableTree<H> {
type Children<'iter>: Iterator<Item=Self>;
fn children<'iter>(&'iter self) -> Self::Children<'iter>;
fn hash(&self, h: &mut H) -> usize;
}
The universal requirement now says that that, whenever we make a child iterator, the hasher H
must outlive 'a
, even though the hasher is not part of the data being iterated. Now I write some generic code that gets a tree
and a hasher
:
fn foo<'x, 'y, T, H>(tree: &'x T, hasher: H) -> T::Children<'x>
where T: EncodableTree<H>
{
tree.hash(hasher);
tree.children()
}
The problem here is that we now have a relationship created between 'x
(the lifetime of the tree we plan to iterate over)
and H
(the type of the hasher). This code would probably fail to compile without a H: 'x
clause (though one could imagine adjusting the rules for implied bounds so that it is implied; currently we don't add implied bounds for projections, since the caller may legitimately normalize them). This bound shouldn't really be needed, in any case.
But I want to pop the stack to one other point. In my original example, I wrote in the trait where Self: 'a
, but I wasn't thinking clearly about the meaning of a where-clause in a trait. Normally, where-clauses in traits are promises that impls make to people using the trait: so if I write trait Foo: Bar
, that means "every impl of Foo
must also be able to show that there is an impl of Bar
". In this way, people who have a bound T: Foo
can rely that T: Bar
must also hold. Similarly, with an associated type, if I write trait Foo { type Output: Hash; }
, I am saying "impls must only provide types that implement Hash
". But that type Iter<'a> where Self: 'a
was saying something different -- it was saying "the impl can assume that Self: 'a
holds".
Currently, the way that impls make assumptions is by adding where-clauses to the impl. But those assumptions are verified at the point where we check that Foo: SomeTrait
-- basically, this is provable if we can find an impl of SomeTrait
for Foo
whose where-clauses are satisfied. But the Self: 'a
constraint is different, because we don't know 'a
at that point. This would be a where-clause which the projector must satisfy, and that is something new.
So, yeah, an interesting question. =) I'm not sure what the best answer is yet. It seems clear that we can't expose this subtlety about who is promising what to whom in surface syntax. So maybe the implicit requirement that "'a
must outlive all input types" is better (though we had similar rules for projections that wound up being too painful, iirc). Or maybe I am not thinking about all this straight and there is an easy solution I am overlooking.