How to allow generic as reference without lifetime

Hi,

I'd like to write a helper to store and trigger callbacks. Some callbacks will pass owned objects and others a references:

struct Listenable<T> {
    callbacks: Vec<Box<dyn Fn(T)>>,
}

impl<T> Listenable<T> {
    pub fn trigger(&self, f: impl Fn() -> T) {
        for callback in self.callbacks.iter() {
            callback(f());
        }
    }
}

// usage

let mut event = 0usize;

let l1 = Listenable::<usize>::new();
l1.trigger(event);

let l2 = Listenable::<&usize>::new(); // requires lifetime for &
l2.trigger(&event);

I understand why L2 and L3 don't work, compiler thinks that struct contains a reference and needs to know its lifetime. But in this case reference lifetime is depended on the lifetime of the argument of trigger function rather than the struct itself.

One solution I see is to have multiple Listenable and ListenableRef but I'd like to avoid duplicating code.

I've also tried to use for<'a> syntax but it doesn't work as well. I don't really understand how it should work for struct, only sample in the book I've found is related to HRTB but it has somewhat different usage.

pub(crate) struct Event<T>
    where for<'a> T: 'a, // I have no idea what I'm doing here...
{
    callbacks: Vec<Arc<SubscriptionInfo<dyn Fn(T)>>>,
}

Is there any way to make it work?

So, to clarify the precise requirements: the two types approach works great and the only concern is how to avoid the code duplication? Maybe you could start by also showing us the exact API of the ListenableRef that fits your use case ^^ Your requirements are not really clear it the demo code you provide to highlight an issue essentially compiles without problems.

Sure, here is an example Rust Playground.

And here are errors:

error[E0106]: missing lifetime specifier
  --> src/main.rs:20:20
   |
20 |     e4: Listenable<&usize>,
   |                    ^ expected named lifetime parameter
   |
help: consider introducing a named lifetime parameter
   |
13 ~ struct MyObject<'a> {
14 |     // these work as expected
 ...
19 |     // these don't work because of the missing lifetimes
20 ~     e4: Listenable<&'a usize>,
   |

error[E0106]: missing lifetime specifier
  --> src/main.rs:21:20
   |
21 |     e5: Listenable<&mut usize>,
   |                    ^ expected named lifetime parameter
   |
help: consider introducing a named lifetime parameter
   |
13 ~ struct MyObject<'a> {
14 |     // these work as expected
 ...
20 |     e4: Listenable<&usize>,
21 ~     e5: Listenable<&'a mut usize>,
   |

It works when Listenable is created as a local variable but cannot be placed in the struct.

Rust has no higher-ranked generics, where you can go from dyn Fn(T) to dyn Fn(&T) (which desugars as dyn for<'any> Fn(&'any T)). There is no for<'any> &'any usize type;[1] &'a usize only resolves to a concrete type given a specific lifetime 'a. Type parameters like T must resolve to a concrete type.

The higher-ranked types that do exist are trait objects, fn pointers, and related unnameable types (function items and closures).

It's not uncommon to just handle the three cases of owned/&/&mut separately. If you don't want to do that, you'll have to change what parts are generic so you can specify a higher-ranked type (Listenable::<dyn Fn(&usize)> or something), or do something like create a trait to generalize over the different cases.

Generalizing over borrowed-or-owned, and generalizing over & or &mut, is sometimes possible but usually at a cost (be it lost inference, ergonomics, or just a big boost in complexity). There's a reason having separate methods and types for owned/shared/exclusive is common.


  1. and if there was, the binder would be in the wrong spot for this case anyway ↩ī¸Ž

3 Likes

Thank, this was a very informative answer. At very least now I understand why I couldn't find solution for this.

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.