Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rust type parameter with HRTB

Tags:

rust

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.

like image 562
Filipe Rodrigues Avatar asked Jul 10 '26 13:07

Filipe Rodrigues


1 Answers

(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.)

like image 62
Filipe Rodrigues Avatar answered Jul 14 '26 19:07

Filipe Rodrigues



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!