Why only T is considered unused?

for the following defination:

    struct AsyncWrapper<F, R, T, U>(F, PhantomData<T>)
    where
        F: Fn(&mut T) -> R + Send + Sync,
        U: Message + 'static,
        T: Message + Default + 'static,
        R: Future<Output = U> + Send + 'static;

only T is considered as unused, thus requires the PhantomData, why?

R and U appear in associated type bounds (e.g. Output = U, function return types desugar to something very similar). Associated types are completely determined by the other types appearing in the bounds, and hence are considered used (assuming the others are used).

2 Likes

The technical term is "constrained".

2 Likes

A more conventional definition of this struct would be

struct AsyncWrapper<F>(F);

The definition of the struct doesn't depend on any of the constraints you'e applying to F, so they should generally not appear there. You'd have to repeat them anyways in the impl blocks, and that's the more appropriate place for them.

The constraints on F, and the additional parameters needed to support them, would normally only be applied to the specific functions that need them, or to the impl blocks containing those functions. You'd have the same problem of unconstrained type parameters in those cases, perhaps, so the other answers are still useful, but I thought I'd point this out. One solution might be shaped like this:

trait Message {}

struct AsyncWrapper<F>(F);

impl<F> AsyncWrapper<F> {
    fn foo<R, T, U>(&mut self) -> ()
    where
        F: Fn(&mut T) -> R + Send + Sync,
        U: Message + 'static,
        T: Message + Default + 'static,
        R: Future<Output = U> + Send + 'static
    {
        unimplemented!()
    }
}
4 Likes

Hmm... I don't understand why

...
F: Fn(&mut T) -> R + Send + Sync,
...

doesn't count as constraint for T according to the rules you mentioned. Maybe I'm completely wrong, but from what I've learned there's also always a compiler generated impl of the Fn trait for the Fn item where as far as I can tell T should be mentioned in the impl trait reference (first rule should apply?).

The first rule is for when you are implementing a trait and only counts for the generic parameters of the trait being implemented.

1 Like

There's nothing stopping you (except for nightly-only features) from having a single "function" that takes multiple argument types:

#![feature(unboxed_closures)]
#![feature(fn_traits)]

struct WeirdFn;

impl FnOnce<(i32,)> for WeirdFn {
    type Output = &'static str;
    extern "rust-call" fn call_once(self, args: (i32,)) -> Self::Output {
        "foo"
    }
}
impl FnOnce<(u32,)> for WeirdFn {
    type Output = &'static str;
    extern "rust-call" fn call_once(self, args: (u32,)) -> Self::Output {
        "bar"
    }
}

fn main() {
    dbg!(WeirdFn(0_i32), WeirdFn(0_u32)); // prints "foo" "bar"
}

Therefore, the bound F: Fn(&mut T) ... does not uniquely determine what T is.

1 Like

You can have such functions on stable too, it's just that you're currently limited to parameters that differ only by lifetimes: for<'a> fn(&'a i32) implements Fn(&'static i32), Fn(&'some_random_lifetime i32), etc etc

2 Likes

I don't know maybe I've just a mental block right now but if you argue like that, you can also say that for eg the AT Rstands for you can choose an arbitrary concrete type:

#![feature(unboxed_closures)]
#![feature(fn_traits)]

struct WeirdFn;

impl FnOnce<(i32,)> for WeirdFn {
    type Output = &'static str;
    extern "rust-call" fn call_once(self, args: (i32,)) -> Self::Output {
        "foo"
    }
}
impl FnOnce<(u32,)> for WeirdFn {
    type Output = String;
    extern "rust-call" fn call_once(self, args: (u32,)) -> Self::Output {
        String::from("bar")
    }
}

impl FnOnce<(f32,)> for WeirdFn {
    type Output = u32;
    extern "rust-call" fn call_once(self, args: (f32,)) -> Self::Output {
        42
    }
}
...
...

Where's the difference? :face_with_spiral_eyes:

R is an associated type, which means that if all the "input" types of the trait bound (F and T in the original post) are constrained then R will be constrained too, because you can't write two implementations for the same trait. i..e the following errors:

#![feature(unboxed_closures)]
#![feature(fn_traits)]

struct WeirdFn;

impl FnOnce<(i32,)> for WeirdFn {
    type Output = &'static str;
    extern "rust-call" fn call_once(self, args: (i32,)) -> Self::Output {
        "foo"
    }
}

// i32 here too!
impl FnOnce<(i32,)> for WeirdFn {
    type Output = String;
    extern "rust-call" fn call_once(self, args: (i32,)) -> Self::Output {
        String::from("bar")
    }
}

Of course this is true only if T was constrained, but if it isn't then you'll get an error for it so it's fine either way.

2 Likes

The difference is in the fact that even if you may supply different T you get back one, and fixed R for each T.

If you pick T you no longer may change R. But T is not similarly constrained.

One may imagine a different language where T and R would be treated symmetrically and where even associated items would be trated similarly to generic params, it's all plausible (read about how Prolog does that, that's not a new thing)… but Rust doesn't work like that.

1 Like

Ahhh that makes sense to me now! :slightly_smiling_face: Thank you very much guys!! :folded_hands: :glowing_star: