I had some time so I took another pass at this.
This may still allow you to be ergonomic for non-context signals with a bit of indirection and scaffolding. Another goal of mine would be to avoid the need for bounds on the struct.
We always have a &mut Context parameter internally, and then use a not-Sized marker type to indicate the no-context case:
type Listener<D, Context> = Box<dyn Fn(&D, &mut Context) + Send + Sync>;
pub enum NoContext {}
#[derive(Clone)]
pub struct Signal<D, TTM, Context: ?Sized = [NoContext]> {
inner: InternalsPtr<D, TTM, Context>,
}
struct SignalInternals<D, TTM, Context: ?Sized = [NoContext]> {
listeners: Vec<Listener<D, Context>>,
delivery_scheduled: bool,
firing: Vec<D>,
thread_transfer_mechanism: TTM,
}
Then we put common code under a Context: ?Sized implementation. The hope is most your code is here to avoid duplication. They can be publically Context shape agnostic, or internal for dispatching from the Context shape sensitive impls:
// Common code
impl<D, TTM, Context: ?Sized> Signal<D, TTM, Context> {
// Publically `Context` agnostic
pub fn schedule(self) { ... }
// Used internally after normalizing
fn add_listener_direct(&self, l: Listener<D, Context>) { ... }
// This one could be public maybe, dunno your use case.
// Public would allow weird looking calls downstream :-)
// So I'd probably only allow it on the with-context API
fn do_stuff_with(&self, ctx: &mut Context) { ... }
}
And then the rest that's necessary to have context and context-free API facades. We use the Sized or not dichotomy to give both cases the same method names.
// Context specific stuff. Note: `Ctx: Sized` so no overlap w/`[NoContext]`.
impl<D, TTM, Ctx> Signal<D, TTM, Ctx> {
pub fn add_listener<F>(&self, f: F)
where F: Fn(&D, &mut Ctx) + Send + Sync + 'static,
{
self.add_listener_direct(Box::new(f));
}
pub fn do_stuff(&self, mut ctx: Ctx) {
self.do_stuff_with(&mut ctx)
}
}
// No context specific stuff
// We normalize by passing/discarding a dummy arg
impl<D, TTM> Signal<D, TTM> {
pub fn add_listener<F>(&self, f: F)
where F: Fn(&D) + Send + Sync + 'static,
{
self.add_listener_direct(Box::new(move |d, _| f(d)));
}
pub fn do_stuff(&self) {
self.do_stuff_with(&mut [])
}
}
So it's ergonomic downstream.
pub fn is_this_usable_1<D, TTM>(s: Signal<D, TTM, String>) {
s.add_listener(|_, s| s.push('?'));
s.do_stuff(String::new());
}
pub fn is_this_usable_2<D, TTM>(s: Signal<D, TTM>) {
s.add_listener(|_| {});
s.do_stuff();
}
The errors when you mess up the arity also seem reasonable.
The reliance on Sized for the generic-with-context case means that any not-Sized context needs its own facade. On the other hand, multiple no-context marker types are possible, so long as they are not Sized.
Usable for you? I'm still unsure, but was happy enough with how it worked out to share.