I have the following type
struct Test<Args> {
// ...
phantom: PhantomData<Args>,
}
that needs to store, alongside it's contents, a type parameter for determining arguments to a function to be passed on later.
I use it as follows:
#![feature(unboxed_closures, fn_traits)]
impl<Args> Test<Args> {
fn update<F>(&mut self, args: Args, f: F)
where
F: FnOnce<Args>
{
//...
let value = f.call_once(args);
// ...
}
}
Unfortunately I cannot use it for borrowed values. I would need to be able to do something like
type MyTest = Test<for<'a> (&'a u32, &'a u32)>;
let mut t: MyTest = ...;
t.update((&5, &8), |lhs, rhs| lhs + rhs);
But Test<for<'a> (&'a u32, &'a u32)> is not valid syntax.
My workaround currently is to modify Test to instead take in an fn type and then restrict it alongside another type at the call site as follows:
#![feature(unboxed_closures, fn_traits)]
struct Test<F> {
// ...
phantom: PhantomData<F>,
}
impl<F> Test<F> {
fn update<Args, F2>(&mut self, args: Args, f: F2)
where
F: FnOnce<Args>,
F2: FnOnce<Args>,
{
//...
let value = f.call_once(args);
// ...
}
}
type MyTest = Test<for<'a> fn(&'a u32, &'a u32)>;
let mut t: MyTest = ...;
t.update((&5, &8), |lhs, rhs| lhs + rhs);
But this is relatively ugly and requires changing the public interface. I'm also not sure if there's any issues with fn being contra-variant in it's arguments.
Is there any better way to achieve this while keeping the original signature (or close to it)?
I don't want to use the call-site F itself in the type generics, as I need it to be generic in the method and not the type, as it can be a closure with borrowed values while the type may be static.
(I'll be using &String instead of &u32 as now &5 is promoted to &'static u32 which doesn't have the problem since it's 'static.)
Ideally Args would actually be a HRT (Higher rank type) of type 'a -> ty (i.e. it receives a lifetime and outputs a type).
Unfortunately this isn't possible in rust currently. However, we can use a workaround with a trait, we can define a WithLifetime trait that functions just like a HRT:
trait WithLifetime {
type Output<'a>: Tuple;
}
(The : Tuple requirement is because Fn<Args> now requires Args: Tuple).
We can them implement it on a new type to serve as the HRT.
struct BorrowedString;
impl WithLifetime for BorrowedString {
type Output<'a> = (&'a String, &'a String);
}
(Note: We could also implement it for (&'static String, &'static String) and use it later on, but this is easier to understand)
After this we can make update use WithLifetime:
impl<Args: WithLifetime> Test<Args> {
fn update<'a, F>(&mut self, args: Args::Output<'a>, f: F)
where
F: FnOnce<Args::Output<'a>>
{
//...
let value = f.call_once(args);
// ...
}
}
Now we can call update twice with temporaries without worrying about the lifetime of the temporaries:
type MyTest = Test<BorrowedString>;
let mut t = ...;
t.update((&"a".to_owned(), &"b".to_owned()), |lhs, rhs| lhs.clone() + rhs);
t.update((&"c".to_owned(), &"d".to_owned()), |lhs, rhs| lhs.clone() + rhs);
Should be noted that for (&String, &String) we can simply use a single 'a lifetime, since &T is covariant, so any (&'a T, &'b T) can be coerced into a (&'c T, &'c T), where 'a: 'c and 'b: 'c (i.e. 'c is shorter or equal than both 'a and 'b).
This is not the case for other types, so WithLifetime isn't enough. You might also want to use a type / const generic within your HRT. The solution is to modify WithLifetime::Output to something like type Output<'l0, 'l1, 'l2, ..., T1, T2, T3, ..., , G1, const V1: G1, G2, const V2: G2> (for the max number of lifetimes / types / const generics you might need. Also note that G1, const V1: G1 isn't currently allowed, so you might define const V1_usize: usize, const V1_u32: u32 ... etc if you really need.)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With