How to pass optional, cloneable callback?

EDIT: I just realized, it also bites when trying to use a Trait Object due to needing Clone, so Box doesn't help in either case, but leaving the question as-is for discussion anyway :slight_smile:

Consider this (playground):

fn foo(cb: Option<impl Fn() + Clone>) {
    if let Some(cb) = cb {
        (cb.clone()) ();
    }
}

fn main() {
    // Fine
    foo(Some(|| {}));
    
    // Not fine
    foo(None);
}

The problem here (picking up from How to specify type parameter when it's unused?), is that I can't really lie about the type there... Box<dyn Fn() + Clone> isn't allowed afaik.

Of course foo could be changed to accept a Box, but I'd prefer to leave the signature of foo as-is.

Suggestions?

You could use the fn() type, which is nameable, Copyable, and Fn()-callable:

foo(None::<fn()>)

But I agree that it is not super pretty. Ideally, non-marker / behavioral-only traits (that is, mainly, traits with just bags of methods) would be auto-implemented for uninhabited types, and then you'd be able to say None::<!> and after monomorphization just get all the code related to that generic parameter and the Some variant Just Disappear™. But correctly defining what is behavioral and what isn't is non-trivial; the least bad option would be to have an magic attribute annotation on traits to mark that.

Until then, the usual option is to have your own empty type for which you implement the required traits, but then in your case you'd still be stuck because of Fn() not being implementable on stable Rust :weary:

6 Likes

thanks, and good tip to use the turbofish syntax too!

Very tangential - I'm surprised this isn't encountered more often or earlier on by people coming to Rust from, say, Javascript/Typescript

Over there, passing an optional callback (i.e. the function type | null | undefined) is extremely bread-and-butter. o_O

I think what I've probably been doing up until now was making all callbacks mandatory, and passing in noops like || {}

I think it's worth to distinguish between closures that are short-lived (passed to a function that uses it immediately, then returns) versus closures that are used as callbacks (stored for later in e.g. a struct and called on some event). In Rust, the former is used widely, but the latter is used very little. Generally I find that the kind of closure that is optional is typically of the latter kind.

1 Like

fwiw these days I'm mostly working in web/wasm with Rust, so this need is much more common, i.e. to stash a callback to be fired on some dom event.

Yeah, I guess this is rather what people give; generally, making a generic parameter optional does not play well, because of None being compatible with any possible generic type instantiation, thus requiring that the type be specified through some annotation somewhere.

I guess the cases that manage to minimize this impact are builder patterns, since you can make Builder::new() already choose some arbitrary type, and then .with_field(…) allow to override that type with that of the actually provided parameter.


By the way, regarding,

it occurred to me that, especially if using a builder pattern, you can create an uninhabited and yet Fn()-callable type :slightly_smiling_face:

fn none ()
  -> Option<impl 'static + Clone + Fn()>
{
    let mut ret = None;
    if false {
        #[derive(Clone)] enum Uninhabited {}
        let _unreachable: Uninhabited = None.unwrap();
        ret = Some(move || { let _ = _unreachable; });
    }
    ret
}

fn main ()
{
    assert_eq!(
        ::core::mem::size_of_val(&none()),
        0,
    );
}

You'd then stumble upon the limitations of min_type_alias_impl_trait not being stable yet (but soon™), so you may have to use the workarounds showcased in this other post:

  • In your case, until option 1. becomes stable, I'd go with 3.
1 Like

good point!

as it happens, the framework I'm using is built entirely around the builder pattern, and that's how the API takes event listeners too: DomBuilder in dominator - Rust

it's too much for me to go back and redesign my little state struct things, but will keep my eye out for the future

1 Like

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.