'Lifetime sub-typing and impl-trait
I bumped into an interesting form of lifetime sub-typing, which I think is valid, but the compiler is skeptical of.
Consider the following function, which computes the dot-product of two sequences of references.
fn dot_prod<'a>(xs: impl IntoIterator<Item = &'a usize>, ys: impl IntoIterator<Item = &'a usize>) -> usize {
let mut acc = 0;
for (x, y) in xs.into_iter().zip(ys.into_iter()) {
acc += *x * *y;
}
acc
}
The signature ascribes the same lifetime to the references in both sequences. The "a single lifetime for both inputs" pattern is common, because sub-typing allows the function to be used on references of different lifetimes. However, something here (perhaps impl trait
?) blocks this. Let's look at a example of a blocked use (playground link):
fn dot_prod_wrap<'a>(xs: impl IntoIterator<Item = &'a usize>, ys: impl IntoIterator<Item = &'a usize>) -> usize {
let xs: Vec<usize> = xs.into_iter().cloned().collect();
let ys = ys.into_iter();
dot_prod(&xs, ys)
}
Rustc rejects this, observing that the local xs
is not valid for 'a
, from which we can infer that it has plugged in 'a
for the function call's lifetime parameter. However, I think this should type-check, by plugging in the local scope's lifetime (call it 'b
), and deducing that ys
has a type which is a sub-typed of something that implements IntoIterator<Item = &'b usize>
, where 'b
is the local scope.
A few variations of this do work, such as changing the impl traits to slices and using two lifetime paramters, but I'm curious about there is a way of getting the Rust compiler to accept a wapper like dot_prod_wrap
without changing the signature of dot_prod
.
I'm also aware that both sub-typing and impl trait
are complex, so perhaps the validity argument I'm presenting above is wrong. The claim "ys
has a type which is a sub-typed of something that implements IntoIterator<Item = &'b usize>
" is particularly suspect.
Solution 1:[1]
Traits are invariant over their generic parameters. Once the borrow checker has deduced impl IntoIterator<Item = &'a usize>
a proper lifetime 'a
it cannot be changed, not even to a shorter lifetime.
The reason is because trait objects are black boxes; they can do anything they want within the bounds of their constraints. This includes things like iterior mutability which cannot use a shorter lifetime (otherwise you'd be able to assign a value with a shorter lifetime to an object expecting a longer one). It works for slices because the borrow checker knows the lifetime is covariant; so it can be made shorter when required.
You should make dot_prod
use two independent lifetimes.
Solution 2:[2]
but I'm curious about there is a way of getting the Rust compiler to accept a wapper like
dot_prod_wrap
without changing the signature ofdot_prod
.
kmdreko's answer is correct about the reasons Rust behaves this way: knowing only that ys
's IntoIterator::IntoIter
type has a certain lifetime isn't enough for it to know whether it can shorten that lifetime. However, that Iterator<Item=&'a usize>
isn't an opaque black box to us programmers:
let ys = ys.into_iter().map(|y| y);
By mapping all the values in the iterator to themselves, we allow Rust to pick an appropriate return type for the closure. After a bit of thinking, rustc
determines that in order to make the dot_prod
call type check, it needs to shorten the lifetime of the returned borrow to match xs
.
.map(|y| y)
doesn't do anything beyond the type system, so rustc
will optimise it out entirely in release builds. (It can do this in all circumstances, since this closure is a zero-sized opaque type. I'm not entirely sure it does in all circumstances, but I can't think of a reason it wouldn't.)
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | kmdreko |
Solution 2 | wizzwizz4 |