Let's consider the following example of a trait involving an FnOnce
callback (but I guess it's the same for FnMut
or Fn
):
use std::cell::RefCell;
use std::rc::Rc;
struct Holder<T>(Rc<RefCell<T>>);
trait Callback<A> {
fn callback(&self, f: impl FnOnce(&A));
}
impl<A> Callback<A> for Holder<A> {
fn callback(&self, f: impl FnOnce(&A)) {
f(&self.0.borrow())
}
}
So far this example allows to write code like this:
let holder_a = Holder(Rc::new(RefCell::new(42)));
holder_a.callback(|a| println!("{a}"));
My goal now is to generalize this pattern over tuples of Holder
, i.e., I'd like to be able to do:
let holder_a = Holder(Rc::new(RefCell::new(42)));
let holder_b = Holder(Rc::new(RefCell::new(1.0)));
(holder_a, holder_b).callback(|(a, b)| println!("{a} {b}"));
and later extend it to larger tuples (perhaps with the typical limit of 12). So the general challenge here is that we have e.g. an (Holder<A>, Holder<B>, Holder<C>)
and for the callback signature we have to provide a (&A, &B, &C)
instance (I don't want to rely on Copy
or Clone
). Essentially the question is how to model the lifetime of these &
in the elements?
Attempt 1
Here are a few naive attempts. In the implementation above, Callback
uses A
as the generic argument, while the FnOnce
uses &A
. One idea would be to turn this around, i.e., don't introduce the &
in the trait already, but "pass it in" via the generic argument. This would lead to:
trait Callback<A> {
fn callback(&self, f: impl FnOnce(A));
}
impl<A> Callback<&A> for Holder<A> {
fn callback(&self, f: impl FnOnce(&A)) {
f(&self.0.borrow())
}
}
impl<A, B> Callback<(&A, &B)> for (Holder<A>, Holder<B>) {
fn callback(&self, f: impl FnOnce((&A, &B))) {
f((&self.0.0.borrow(), &self.1.0.borrow()))
}
}
Basically the signatures look right, but the compiler complains: impl has extra requirement for<'a> impl FnOnce(&A): FnOnce(&'a A)
. It sound like this is not allowed because the trait itself has no lifetime associated to the FnOnce
, but the impls do.
Attempt 2
My next attempt was to go even more generic, and also pass in the lifetime:
trait Callback<'a, Args> {
fn callback(&'a self, f: impl FnOnce(Args));
}
impl<'a, A> Callback<'a, &'a A> for Holder<A> {
fn callback(&'a self, f: impl FnOnce(&'a A)) {
f(&self.0.borrow())
}
}
impl<'a, A, B> Callback<'a, (&'a A, &'a B)> for (Holder<A>, Holder<B>) {
fn callback(&'a self, f: impl FnOnce((&'a A, &'a B))) {
f((&self.0.0.borrow(), &self.1.0.borrow()))
}
}
Almost: This time the relation between the trait and the impls is fine, and also the intended usage compiles. But, the compiler rightfully complains that the lifetime of the borrow doesn't last long enough (creates a temporary value which is freed while still in use). Indeed, I guess now 'a
corresponds to the lifetime of &'a self
and the borrow is more short lived than that.
The question remains: How can I model these lifetimes (&'a A, &'a B, ...)
correctly in such a case? Is there a neat trick leveraging GATs or HRTBs?
You can do that using both GAT and HRTB, but it won't necessarily be convenient: