1

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.

Chayim Friedman
  • 47,971
  • 5
  • 48
  • 77
SkyPPeX
  • 25
  • 4
  • Related: [Should trait bounds be duplicated in struct and impl?](https://stackoverflow.com/questions/49229332/should-trait-bounds-be-duplicated-in-struct-and-impl) – Chayim Friedman Oct 31 '22 at 20:02

2 Answers2

2

In rust your struct must use all of the generic types that it is generic over. And use mean, that they must appear in at least one type of the field. You can solve your problem with special type PhantomData. It is a marker type that is used to provide additional information to the compiler and is removed at compile time. The docs give you even example of how to use it to solve "unused type parameters" error. TLDR is here:

use std::marker::PhantomData;

pub struct Foo<TFn, TArg, TReturn>
where
    TFn: Fn(TArg) -> TReturn
{
    func: TFn,
    _marker: PhantomData<(TArg, TRetur)> // This line tells the compiler that
                                         // this struct should act like it owned
                                         // type (Targ, TReturn)
}

And when you want to create an instance of your struct just put PhantomData as a value:

let s = Foo { func: f, _marker: PhantomData };
Aleksander Krauze
  • 3,115
  • 7
  • 18
  • 1
    It is better to remove the constraint from the struct declaration. – Chayim Friedman Oct 31 '22 at 20:01
  • While this does make the compiler shut up, `Foo` doesn't own a `TArg` or a `TReturn` and shouldn't act like it either. I have posted an answer that takes variance into consideration below. – isaactfa Oct 31 '22 at 20:29
2

@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.

isaactfa
  • 5,461
  • 1
  • 10
  • 24
  • This is presicely what i was looking for. It seemed to me almost like an oversight that what i was trying to do wouldn’t work, but from this explanation i understand how allowing would cause issues as the compiler would then have to default to either covariance or contravariance. I guess it could be solved with a keyword somewhere too just for ease of use. Regardless, thanks for the detailed explanation. In the end the struct ended up doing a bit more than was shown here, and it used all the generics in the fields of the struct, which actually made my code even more flexible in the end. – SkyPPeX Nov 01 '22 at 23:14