<Into>-like trait can't infer types even with result type

I'm trying to create a generic task runner for a data source that allows for functions that accept generic fetched parameters.

However I run into 2 problems - type inference for the IntoSystem trait and lifetime issues if any parameters use lifetimes.

Any help is much appreciated.

Playground - whole example here

Here is what i would like to work...

1 - Declare some work functions

/// A work function 
fn sys_source(source: &Source) {
    println!("source system - {}", source.age.borrow());
}

/// A fetch work function
fn sys_u32(age: u32) {
    println!("age system - {}", age);
}

/// A fetch work function using a reference
fn sys_ref(age: Ref<u32>) {
    println!("ref system - {}", *age);
}

2 - Convert them to systems using the trait IntoSystem

    let sys_a: System = sys_source.into_system();   // Works
    let sys_b: System = sys_u32.into_system();      // error[E0282]: type annotations needed
    let sys_c: System = sys_ref.into_system();      // error[E0282]: type annotations needed

3 - Run them

    sys_a.run(&source);
    sys_b.run(&source);
    sys_c.run(&source);

So there are 2 questions here:

Question 1 - What has to change to make the simpler A.into_system() format work?

    let sys_b: System = sys_u32.into_system();      // error[E0282]: type annotations needed
    let sys_b: System = <fn(u32) as IntoSystem<(u32,)>>::into_system(sys_u32); // Works

Question 2 - Even with that change, the lifetimes of references don't work. What has to change to make the lifetime limited fetches work?

    let sys_c: System = sys_ref.into_system();      // error[E0282]: type annotations needed
    let sys_c: System = <fn(Ref<u32>) as IntoSystem<(Ref<u32>,)>>::into_system(sys_ref); // error: implementation of `Fetch` is not general enough
    // = note: `Fetch<'0>` would have to be implemented for the type `Ref<'_, u32>`, for any lifetime `'0`...
    // = note: ...but `Fetch<'1>` is actually implemented for the type `Ref<'1, u32>`, for some specific lifetime `'1`

Here is my System and IntoSystem implementations:


/// A function that operates on a Source
struct System {
    func: Box<dyn Fn(&Source) -> ()>,
}

impl System {
    /// Construct a new System with the given work function
    fn new(func: Box<dyn Fn(&Source) -> ()> ) -> Self {
        System { func }
    }

    /// Run the system's work function
    fn run(&self, source: &Source) {
        (self.func)(source);
    }
}

/// Converts a type into a system
trait IntoSystem<A> {
    fn into_system(self) -> System;
}

/// Make a system for a Source level func
impl<F> IntoSystem<&Source> for F
where
F: Fn(&Source) -> () + 'static
{
    fn into_system(self) -> System {
        System::new(Box::new(move |source| {
            (self)(source);
        }))
    }    
}

/// Make a system that takes a single fetch parameter
impl<F,A> IntoSystem<(A,)> for F
where 
for<'a> A: Fetch<'a>,
for<'a> F: Fn(<A as Fetch<'a>>::Output) -> () + 'static
{
    fn into_system(self) -> System {
        System::new(Box::new(move |source| {
            let data = A::fetch(source);
            (self)(data);
        }))
    }    
}

/// Make a system that takes 2 fetch parameters
impl<F,A,B> IntoSystem<(A,B)> for F
where 
for<'a> A: Fetch<'a> + 'a,
for<'a> B: Fetch<'a> + 'a,
for<'a> F: Fn(<A as Fetch<'a>>::Output, <B as Fetch<'a>>::Output) -> () + 'static
{
    fn into_system(self) -> System {
        System::new(Box::new(move |source| {
            let data = <(A,B)>::fetch(source);
            (self)(data.0, data.1);
        }))
    }    
}

And here is the Fetch stuff...


/// Trait to allow fetching arbitrary parts of the Source
trait Fetch<'a> {
    type Output;
    fn fetch(source: &'a Source) -> Self::Output;
}

/// Fetch the age as a u32
impl<'a> Fetch<'a> for u32 {
    type Output = u32;
    
    fn fetch(source: &'a Source) -> Self::Output {
        *source.age.borrow()
    }
}

/// Fetch the age as a Ref<u32>
impl<'a> Fetch<'a> for Ref<'a, u32> {
    type Output = Ref<'a, u32>;
    
    fn fetch(source: &'a Source) -> Self::Output {
        source.age.borrow()
    }
}


/// Fetch any item as a single tuple
impl<'a, A> Fetch<'a> for (A,) 
where A: Fetch<'a> + 'a
{
    type Output = (<A as Fetch<'a>>::Output,);
    
    fn fetch(source: &'a Source) -> Self::Output {
        (A::fetch(source),)
    }
}

/// Fetch any item as a pair tuple
impl<'a, A, B> Fetch<'a> for (A,B) 
where 
A: Fetch<'a> + 'a,
B: Fetch<'a> + 'a
{
    type Output = (<A as Fetch<'a>>::Output,<B as Fetch<'a>>::Output);
    
    fn fetch(source: &'a Source) -> Self::Output {
        (A::fetch(source),B::fetch(source))
    }
}

The first culprit should be clear from the trait definition itself:

trait IntoSystem<A> {
    fn into_system(self) -> System;
}

Nothing in the trait actually uses the type parameter A. Just remove it.

2 Likes

For the second problem,

impl<F, A, B> IntoSystem<(A, B)> for F
where
    for<'a> A: Fetch<'a> + 'a,
    for<'a> B: Fetch<'a> + 'a,
    for<'a> F: Fn(<A as Fetch<'a>>::Output, <B as Fetch<'a>>::Output) -> () + 'static,

Type parameters like A have to resolve into a concrete type, not one with a free lifetime parameter like Ref<'_, u32>. And indeed this:

impl<'a> Fetch<'a> for Ref<'a, u32> {

Means there is no 'x for which this bound holds:

Ref<'x, u32>: for<'a> Fetch<'a> + 'a

Being generic over borrowing or owned input types often breaks inference. Sometimes you can restore inference by being less generic in your implementations. Perhaps you can find some inspiration here.

1 Like

@quinedot - Thanks for the help. The link you provided helped a lot with the Fetch part.

I changed it to the following, which removed the lifetime:


trait MaybeBorrowed<'a> {
    type Output: 'a + Sized;
}

/// Retrieve/borrow value from container
trait Fetch: for<'a> MaybeBorrowed<'a> {
    fn fetch(source: &Source) -> <Self as MaybeBorrowed<'_>>::Output;
}

impl<'a> MaybeBorrowed<'a> for u32 {
    type Output = u32;
}
impl Fetch for u32 {
    fn fetch(source: &Source) -> <Self as MaybeBorrowed<'_>>::Output {
        *source.age.borrow()
    }
}

impl<'a> MaybeBorrowed<'a> for Ref<'_, u32> {
    type Output = Ref<'a, u32>;
}
impl Fetch for Ref<'_, u32> {
    fn fetch(source: &Source) -> <Self as MaybeBorrowed<'_>>::Output {
        source.age.borrow()
    }
}

impl<'a> MaybeBorrowed<'a> for RefMut<'_, u32> {
    type Output = RefMut<'a, u32>;
}
impl Fetch for RefMut<'_, u32> {
    fn fetch(source: &Source) -> <Self as MaybeBorrowed<'_>>::Output {
        source.age.borrow_mut()
    }
}

impl<'a, A> MaybeBorrowed<'a> for (A,)
where
    A: MaybeBorrowed<'a>,
{
    type Output = (<A as MaybeBorrowed<'a>>::Output,);
}
impl<A> Fetch for (A,)
where
    A: Fetch,
{
    fn fetch(source: &Source) -> <Self as MaybeBorrowed<'_>>::Output {
        (A::fetch(source),)
    }
}

impl<'a, A, B> MaybeBorrowed<'a> for (A, B)
where
    A: MaybeBorrowed<'a>,
    B: MaybeBorrowed<'a>,
{
    type Output = (
        <A as MaybeBorrowed<'a>>::Output,
        <B as MaybeBorrowed<'a>>::Output,
    );
}
impl<A, B> Fetch for (A, B)
where
    A: Fetch,
    B: Fetch,
{
    fn fetch(source: &Source) -> <Self as MaybeBorrowed<'_>>::Output {
        (A::fetch(source), B::fetch(source))
    }
}

Still working on the IntoSystem part.

These days you can probably roll that into a GAT:

// If you need it independently of `Fetch`
trait MaybeBorrowed {
    type Output<'a>;
}

trait Fetch: MaybeBorrowed {
    fn fetch(source: &Source) -> <Self as MaybeBorrowed>::Output<'_>;
}
// If you don't
trait Fetch: MaybeBorrowed {
    type Output<'a>;
    fn fetch(source: &Source) -> Self::Output<'_>;
}

(But I have hit a couple situations that I got to work by falling back on a lifetime-parameterized trait.)


I might take another look at the bigger picture later.

I haven't taken the time to clean it up or move to GATs, but here's where I got with your latest playground.

  • I kept this implementation
    impl<F> IntoSystem<&Source> for F
    where
        F: Fn(&Source) -> () + 'static,
    
  • I added an implementation for non-borrowed single-arg functions
    impl<F,A> IntoSystem<(A,)> for F
    where 
        A: Fetch + for<'x> MaybeBorrowed<'x, Output = A>,
        F: Fn(A) -> () + 'static,
    
  • I added an implementation for Ref<'_, u32> single-arg functions
    // Using `Ref<'_, u32>` does not resolve the ambiguity errors and also triggers
    // a `coherence_leak_check` warning.  https://github.com/rust-lang/rust/issues/56105
    // I.e. the code using `Ref<'_, 32>` may or may not continue to be accepted
    struct AllRef;
    impl<F> IntoSystem<(AllRef,)> for F
    where 
       F: Fn(Ref<'_, u32>) -> () + 'static,
    

I used a static representative (AllRef) for the reasons in the comments. (I assume the reason for the unconstrained type parameter on IntoSystem is to avoid overlapping implementations on your generic closures types.)

At this point, sys_a and sys_b work as desired but sys_c still needs annotations, probably because the type parameter is unconstrained and there's only an indirect connection between the inputs to into_system and the type parameter.

EDIT: I found a fix for sys_c, see next post and optionally skip the rest of this one

  • So I also added a helper method to the trait
     // Needs bikeshed
    fn into_system_via_representative(self, _representative: A) -> System 
    where 
        Self: Sized
    {
        self.into_system()
    }
    
    let sys_c = sys_ref.into_system_via_representative((AllRef,));
    

I tried a couple variations like removing the tuple without any obvious benefits beyond ergonomics of the helper method. If you used Ref<'_, u32> instead of AllRef and/or if you used the generic implementation where sys_b also needs annotations, you may want to use PhantomData<A> in the helper instead... although that's another ergonomic hit.

Supporting higher arities can make the number of implementations explode when you try to cover every combination of borrowed-or-not.


Taking a step back, I don't find these too onerous:

impl System {
    fn new<F: Fn(&Source) + 'static>(f: F) -> Self {
        let func = Box::new(f);
        Self { func }
    }
}

fn main() {
    let sys_a = System::new(sys_source);
    let sys_b = System::new(|s| sys_u32(*s.age.borrow()));
    let sys_c = System::new(|s| sys_ref(s.age.borrow()));
}

But I might be missing some higher goal of yours.

1 Like

I ran into an ICE when playing around with this earlier, and in the process of reducing it I noticed a difference in your MaybeBorrowed code from what I expected (it got lost in all the noise before):

Here, any Ref<'r, u32> can "represent" a higher-ranked argument of Ref<'_, u32>; the compiler is eventually going to need to pick one such representative, and the lifetime is going to be arbitrary. It's sort of like when you have

fn foo<T>() {}

except the compiler is sometimes willing to pick an arbitrary lifetime, but never an arbitrary type.

In this case it's giving the compiler too much "freedom", so much it gives up inferring things because it has too many free variables. Instead you can use a concrete representative similarly to how I did for the implementation:

impl<'a> MaybeBorrowed<'a> for AllRef {
    type Output = Ref<'a, u32>;
}
impl Fetch for AllRef {
    fn fetch(source: &Source) -> <Self as MaybeBorrowed<'_>>::Output {
        source.age.borrow()
    }
}
    // You'll have to use the representative in turbofish
    let (age,age_ref) = <(u32,AllRef)>::fetch(&source);

    // But these all work now
    let sys_a: System = sys_source.into_system();
    let sys_b: System = sys_u32.into_system(); 
    let sys_c = sys_ref.into_system();

With that I think it's roughly in the same state as that other thread, modulo being cleaned up perhaps.

1 Like

Thanks for coming back to this and trying again.

It will take me a little while to see if I can adapt the AllRef version into what i am really trying to do. I think that the generics are going to cause problems. And, at this point, it feels like a lot of very complicated compiler wrangling in order to save a 2-3 line wrapper function.

Thanks again for the help - especially with the GAT stuff.

I ended up simplifying it down to;

pub trait Fetch {
    type Output<'a>;
    fn fetch(source: &Source) -> Self::Output<'_>;
}

impl Fetch for &Source {
    type Output<'a> = &'a Source;
    fn fetch(source: &Source) -> Self::Output<'_> {
        source
    }
}

impl<T> Fetch for Ref<'_, T>
{
    type Output<'a> = Ref<'a, T>;
    fn fetch(source: &Source) -> Self::Output<'_> {
        source.age.borrow()
    }
}

// ...

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.