Generic bounds of a struct with a Vec of closures

So I've tried a few permutations of the generic bounds on the Signal struct below but the only way I can get things to be happy is using T as a member of the struct. I know about "associated types" on traits (such as Item on Iterator) but I don't think I can use that on a struct. What is the idiomatic way to do what I am trying to do below? Or maybe it's just not meant to be and there is just another way?

As it's written now it's saying that T is not used in the struct. T is only used in the bound of F, but if I remove T from the generics list then the F bound breaks.

struct Signal<T, F: FnMut(&T) -> ()> {
    subscribers: Vec<F>,
}

impl<T, F: FnMut(&T) -> ()> Signal<T, F> {
    fn new() -> Self {
        Signal {
            subscribers: vec![],
        }
    }

    fn emit(&mut self, arg_type: &T) {
        for handler in &mut self.handlers {
            handler(arg_type)
        }
    }

    fn subscribe(&mut self, handler: F)
    where
        F: FnMut(&T) -> (),
    {
        self.handlers.push(handler)
    }
}

Thanks for the help!
Dave

It wouldn't be a good idea to make F a type parameter of Signal. Each closure in a program has its own automatically-generated type, so the user could only put in the exact same closure over and over again. Instead, you can store your subscriber list as a vector of boxed trait objects, of type dyn FnMut(&T). Then, since T is used, you can write a definition of Signal<T>:

struct Signal<T> {
    subscribers: Vec<Box<dyn FnMut(&T)>>,
}

impl<T> Signal<T> {
    fn new() -> Self {
        Signal {
            subscribers: vec![],
        }
    }

    fn emit(&mut self, arg_type: &T) {
        for handler in &mut self.subscribers {
            handler(arg_type)
        }
    }

    fn subscribe<F>(&mut self, handler: F)
    where
        F: FnMut(&T) + 'static,
    {
        self.subscribers.push(Box::new(handler))
    }
}

This way, any closure can be added as a handler, so long as it takes an &T parameter. But note the 'static bound on the subscribe() method; it means that the handler cannot borrow any data, so we can hold onto it for however long we want.

3 Likes

You can use PhantomData:

struct Signal<T, F> {
    subscribers: Vec<F>,
    _marker: std::marker::PhantomData<T>,
}

impl<T, F: FnMut(&T)> Signal<T, F> {
    fn new() -> Self {
        Signal {
            subscribers: vec![],
            _marker: std::marker::PhantomData,
        }
    }

    fn emit(&mut self, arg_type: &T) {
        for handler in &mut self.subscribers {
            handler(arg_type)
        }
    }

    fn subscribe(&mut self, handler: F) {
        self.subscribers.push(handler)
    }
}

I don't think there is a better way.

Thank you both for the replies. @chrefr, I had seen a hint for PhantomData in the compiler message, I should have looked further into it. In my naivete, since I had not come across it before, I assumed it was for a more advanced and specialized use case. It is now more clear what this can do.

@LegionMammal978, I had forgotten that each closure gets a unique type definition at compile time, I had read that in Blandy's book. That is not true of function definitions is it? Even still I think I agree that is probably makes more sense to only be generic over T in this case. There isn't much that is "generic" about the callable itself, rather what the callable takes as it's argument.

Thanks for the responses, this forum and community are an incredible resource!

Dave

Technically, each function definition also has its own type. But all functions can be cast to a function pointer of type fn(Args...) -> Ret. Closures can also be cast to a function pointer (RFC 1558), but only if they capture no data from their environment, by reference or by value. Since it is often useful for closures to refer to outside data, APIs generally keep them as Fn* objects.

3 Likes

Got it, thanks!

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.