I have a trait (we'll call it WrapFn) that is being implemented for all functions whose parameters satisfy FromValue<'a> and return types satisfy IntoValue<'a>. The point of this trait is to convert a function with complex types, e.g.
// using f64::sin as an example
fn sin(x: f64) -> f64;
into a generic function which takes a Value<'a> and returns a Value<'a>.
The only method of WrapFn is wrap_fn(self) -> FnWrapper<impl for<'a> Fn(Value<'a>) -> Value<'a>>. Currently, the implementation looks like this:
/// this will use variadics_please to implement for all functions of arity > 1
impl<
P0: for<'a> FromValue<'a>,
R: for<'a> IntoValue<'a>,
F: Fn(P0) -> R,
> WrapFn<(P0, R)> for F {
fn wrap_fn(self) -> FunctionWrapper<impl for<'a> Fn(Value<'a>) -> Value<'a>> {
FunctionWrapper {
function: move |v| {
self(P0::from_value(v)).into_value()
}
}
}
}
This, however, does not work because types that use a reference to the original value, e.g. Value<'a> itself, are not guaranteed to implement for<'b> IntoValue<'b>. Therefor, the following method does implement WrapFn.
fn identity<'a>(value: Value<'a>) -> Value<'a>;
My guess is that if I am somehow able to specify for<'a> Fn(impl FromValue<'a>) -> impl IntoValue<'a> (which is invalid syntax), while retaining access to the argument/return types, it will work.
I have tried:
for<'a> on both the P0 and R generic types in the impl block. didn't work because the lifetimes are different for P0 than for R.
impl<'a> doesn't work because the function must eventually fit inside for<'a> Fn(Value<'a>) -> Value<'a> and can't be tied to a specific lifetime. (wrap_fn is called in a constant, so I don't have access to the real lifetime)
making another trait that looks like this: trait Arity1Transformer<'a>: Fn(Self::P0) -> Self::R where type P0: From<Value<'a>> and type R: Into<Value<'a>> . This doesn't work because you can't access the associated types without specifying a lifetime.
Here's a playground link demonstrating the problem: Rust Playground
Yeah, this tentatively looks like it requires “higher-kinded types”. Imagine Vec without a <T> or &'varying T where you can feed in a concrete lifetime for 'varying later. Rust doesn’t properly support these, but you can emulate them with generic associated types (GATs).
For example, you could define
trait FromValue {
// This is a GAT
type Output<'a>;
fn from_value(value: Value<'_>) -> Self::Output<'_>;
}
I tried to implement this your way, but I couldn't figure it out. Here's as far as I got: Rust Playground.
I'm not sure this will work because there's no easy way to differentiate which FromValue implementation the WrapFn trait should use.
// First implementation of FromValue<Output = bool>
impl FromValue for () {
type Output<'a> = bool;
fn from_value(_value: Value<'_>) -> Self::Output<'_> {
false
}
}
// Second implementation of FromValue<Output = bool>
impl FromValue for u64 {
type Output<'a> = bool;
fn from_value(_value: Value<'_>) -> Self::Output<'_> {
true
}
}
// If we consider the implementation for `fn(bool) -> Value<'a>`, is it passed true or false?
impl<
P0: FromValue,
R: IntoValue,
F: for<'a> Fn(P0::Output<'a>) -> R::Input<'a>,
> WrapFn<(P0, R)> for F
{
// ...
}
Also, identity::<Value<'_>> works for only one lifetime, not any lifetime, so that cannot work. Even |x| x will not be inferred to be a higher-order closure unless you force it to be; you’d need to wrap it in something like fn coerce<F>(f: F) where F: for<'a> /* whatever */ { f }.
This reminds me of this exercise[1] or perhaps this topic. You can get it to work to some extent, but there's usually a significant inference hit and/or a ton of boilerplate, especially when you extend arity. You may also have to make some compromises if you're going to try to take multiple args but return a single Value<'_> somehow (it's not clear to me what you want from larger arities).
For example, one way to work around this is by starting with single lifetime traits and wrapping those up in higher-ranked lifetime traits, but it will completely ruin inference. If you can find a way to get the Fn bound back to being directly on the implementation, that usually helps inference, but may involve compromises like the PhantomData suggestion,[2] or implementing for specific type constructors instead of generically.