I read @withoutboats' recent post about generators and was thinking about cases where you might want to borrow over a yield point and what could be done about it. It seems to me that a lot of the cases that would be problematic would be when the generator owns some object that has data on the heap and wants to hold a non-consuming iterator over yield points. For example, you’ve got a String and want to hold a Chars<'a>
iterator. e.g.
gen fn foo(&self) -> Token {
let s: String = self.to_string();
for ch in s.chars() {
match ch {
'a' => yield Token::Something,
…
}
}
}
This isn’t valid because s.chars()
returns a Chars<'a>
which borrows s
which is owned by the generator. However, it should actually be fine. Moving the generator will do nothing to the heap allocation of the String
and we never mutate the String, so the references held by the Chars<'a>
won’t be invalidated.
What I’m wondering is, could the borrow checker be made smart enough to allow this? I was thinking of a special kind of “indirect borrow” that is like a regular borrow, but with the relaxation that a shared, indirect borrow can be held over a yield point. i.e. if String::chars
returned a Chars<'indirect a>
(or without a new keyword, Chars<'*a>
) then the generator could be moved and the borrow over the yield permitted. Effectively, if the function passes the borrow checker as a regular (non-generator) function and also passes the additional requirement that only indirect borrows are held over yield points, then the generator passes the borrow checker.
A fully general system (e.g. this) that allowed arbitrary structs to take advantage of this in order to have self referential structs seems like it would add a lot more complexity to the syntax, but just limiting this to generators holding shared, indirect borrows seems more tractable to me. It would only require one new bit of syntax (declaring a lifetime as indirect).
Changing an existing inherent method or function to mark its return lifetime as indirect, should I think be a non-breaking change. I guess one downside is that changing a trait method’s return type to an indirect lifetime would be a breaking change, so the return value from Deref::deref couldn’t be indirect - although you wouldn't want it to be indirect anyway, since that would prevent implementing Deref for anything that didn’t use heap allocation.
If generators were stabilised with no support for borrowing over yields, something like this could be kept as a potential relaxation of that requirement if not allowing borrows over yields turned out to be too restrictive.