Search code examples
rustcompiler-errorssyntax-error

How can i constrain a generic constraint using a different generic argument?


So i have a struct. pub struct Foo<TFn, TArg, TReturn> where TFn: Fn(TArg) -> TReturn { func: TFn }

This makes sence in my head being used to C# Generics, but why doesn't it work in rust? I want the field 'func' to be of type Fn where the argument is of type 'TArg' and the return value is of type 'TReturn'.

The compiler is complaining that the paramter 'TArg' and 'TReturn' are never used, but they are helping to define the signature of the TFn value.

I tried removing the 'never used' parameters and just writing in a type in the constraint explicitly. That works fine.


Solution

  • @Aleksander Krauze's answer is right, but doesn't take into account one niche concern: variance. Consider this:

    fn x<'s>(&'s str) -> String { // ...
    

    x is a function that can deal with any &str that lives for 's. In particular this means that x can deal with any &str that lives longer than 's, a &'static str for example. This is true because &'static str is a subtype of &'s str. In Rust, T being a subtype of U (written T: U) means wherever I can accept a U, I can also accept a T, because a subtype can do anything its supertype can do and more (live longer, for example).

    Subtyping comes up most often for lifetimes in Rust, where lifetime 'a is a subtype of lifetime 'b if 'a outlives (i.e. is longer than) 'b. So if I can accept a &'b T, I can also accept a &'a T as long as 'a: 'b. In other words, &'a T is a subtype of &'b T if 'a is a subtype of 'b. This is called covariance and is an instance of what is called variance.

    However, functions are interesting in this regard. If I have types fn(&'a T) and fn(&'b T), fn(&'a T) is a subtype of fn(&'b T) if 'b is a subtype of 'a not the other way around. I.e. if I need a function that can deal with long lifetimes, then a function that can deal with shorter lifetimes will also do, because any argument I can pass it will be a subtype of the argument it expects. This is called contravariance and is a property we very much want for our functions.

    Your type Foo is more or less a function so we'd like it to behave like one and be contravariant over its argument. But it isn't. It's covariant. That's because a struct in Rust inherits its variance from its fields. The type PhantomData<(TArg, TReturn)> is covariant over TArg and TReturn (because the type (TArg, TReturn) is) and so Foo will be covariant over TArg. To get it to behave like a function should, we can just mark it with the appropriate type: PhantomData<fn(TArg) -> TReturn>. This will be contravariant over TArg and covariant over TReturn (functions are covariant over their return type; I hope that follows from the explanations above).

    I've written a little example (albeit an artificial one) to demonstrate how incorrect variance can break code that should work:

    use std::marker::PhantomData;
    
    pub struct Foo<TFn, TArg, TReturn>
    {
        func: TFn,
        // this makes `Foo` covariant over `TArg`
        _marker: PhantomData<(TArg, TReturn)>
    }
    
    impl<TFn, TArg, TReturn> Bar<TFn, TArg, TReturn>
    where
        TFn: Fn(TArg) -> TReturn,
    {
        // presumably this is how one might use a `Foo`
        fn call(&self, arg: TArg) -> TReturn {
            (self.func)(arg)
        }
    }
    
    // `foo_factory` will return a `Foo` that is covariant over the lifetime `'a`
    // of its argument
    fn foo_factory<'a>(_: &'a str) -> Foo<fn(&'a str) -> String, &'a str, String> {
        // We only care about the type signatures here
        panic!()
    }
    
    fn main() {
        let long_lifetime: &'static str = "hello";
    
        // we make a `Foo` that is covariant over the `'static` lifetime
        let mut foo = foo_factory(long_lifetime);
    
        foo.call("world");
    
        {
            let short_lifetime = String::from("world");
            // and because it's covariant, it can't accept a shorter lifetime
            // than `'static`
            // even though this should be perfectly fine, it won't compile
            foo = foo_factory(&short_lifetime);
        }
    
        foo.call("world");
    }
    

    But if we fix the variance:

    pub struct Foo<TFn, TArg, TReturn> {
        func: TFn,
        // `Foo` is now _contravariant_ over `TArg` and covariant over `TReturn`
        _marker: PhantomData<fn(TArg) -> TReturn>,
    }
    

    The main function from above will now compile just fine as one would expect.

    For more on variance in Rust and how it relates to data structures and the drop check, I recommend checking out the 'nomicon chapter on it and the one on PhantomData.