Search code examples
genericsrusteventstypeslifetime

Rust generic lifetime elision issue


I am trying to a implement generic observable collections for use with a UI framework I'm writing. I'm currently having an issue with lifetimes not being elidable in generics.

For brevity the following Callback and Event types not the full event framework but the issue should still be highlighted.

type Callback<Args> = Arc<dyn Fn(&Args)>;

struct Event<Args> {
    handlers: Vec<Callback<Args>>,
}

struct OnChangedArgs<'a, T> {
    items: std::slice::Iter<'a, T>,
}

struct ObservableList<T> {
    items: Vec<T>,
    on_changed: Event<OnChangedArgs<'_, T>>,
                                // ^^^^^ can't elid lifetime
}

I can resolve my issue by writing my event system in a non generic manner but that would may code duplication highly annoying.

struct OnChangedArgs<'a, T> {
    items: std::slice::Iter<'a, T>,
}

pub type OnChangedCallback<T> = Arc<dyn Fn(&OnChangedArgs<T>)>;
                                                     // ^^^^^ lifetime elided

struct OnChangedEvent<T> {
    handlers: Vec<OnChangedCallback<T>>,
}

struct ObservableList<T> {
    items: Vec<T>,
    on_changed: OnChangedEvent<T>,
                               
}

Am I dumb or does this seem to be an annoyance with the Rust type system???

It just seems like the type system should be able to see through Event to see that it can elid the lifetime. That way you don't have to unnecessarily bubble the lifetime up so that you have to deal with it higher up in another struct.


Solution

  • I don't know if you will like that (or if it is a good idea anyway), but there is a way to make the lifetime higher-ranked (therefore eliding it), by using Generic Associated Types:

    use std::sync::Arc;
    
    type Callback<Args> = Arc<dyn Fn(&<Args as EventArgs>::Gatified<'_>)>;
    
    trait EventArgs {
        type Gatified<'a>;
    }
    
    struct Event<Args: EventArgs> {
        handlers: Vec<Callback<Args>>,
    }
    
    impl<Args: EventArgs> Event<Args> {
        fn trigger(&self, args: &Args::Gatified<'_>) {
            for handler in &self.handlers {
                handler(args);
            }
        }
    }
    
    struct OnChangedArgs<'a, T> {
        items: std::slice::Iter<'a, T>,
    }
    
    impl<T: 'static> EventArgs for OnChangedArgs<'static, T> {
        type Gatified<'a> = OnChangedArgs<'a, T>;
    }
    
    struct ObservableList<T: 'static> {
        items: Vec<T>,
        on_changed: Event<OnChangedArgs<'static, T>>,
    }
    
    impl<T: 'static> ObservableList<T> {
        fn notify(&self) {
            self.on_changed.trigger(&OnChangedArgs {
                items: self.items.iter(),
            });
        }
    }
    

    OnChangedArgs<'static> serves as a marker type (it could also be a different type) that enables the GATification of any OnChangedArgs.