How to "erase" generic type in function arguments

The origin of this question is me playing with Leptos and (as always :/) being screamed at by the compiler at the third line of code. While there is probably a Leptos-specific way of solving my problem I'd like to understand what options do I have in a more generic context.

I have a function that takes an optional closure Option<F> and I need to call it passing None as the argument. Obviously the compiler wants to know the type of F and with a simple F I can just pass the correct type. But in Leptos components, my types are a bit more complex (enough that specifing the type is not trivial, at least for me) and it is possible to call a component without the argument and have it replaced by the default (i.e., None), a feature that makes component instantiation very ergonomic except that it does not work if generics are involved.

I was thinking about using some kind of wrapper type or trait that does not expose the generic F and let me pass just None instead of None::<T> but I havent found a solution yet. Does anybody knows how to do that?

Full code:

fn take_optional_f<F>(f: Option<F>, value: i32) -> bool
where
    F: Fn(i32) -> bool + 'static
{
    if let Some(f) = f {
        f(value)
    }
    else {
        value % 2 == 0
    }
}

fn main() {
    // The compiler does not like this. Removing the comment makes it work.
    assert_eq!(take_optional_f(None /* ::<fn(i32) -> bool> */, 1), false);
}

(Playground)

Example of why I stumbled on this in Leptos:

#[component]
fn Button<F, IV>(
    #[prop(optional)] icon: Option<F>,
    // other "props", not important right now
) -> impl IntoView
where
    F: Fn() -> IV + Send + Sync + Clone + 'static,
    IV: IntoView + 'static,
{
    // Button implementation here.
}

fn UseButton() -> impl IntoView {
    // Note that we don't need to pass "icon" here, but obviously
    // it will not work because the compiler doesn't know about
    // the type of `F` above.
    view! {
        <Button class="filled" />
    }
}

Are there a lot of variations on the bounds or just a few? If just a few, you could consolidate the pain to a a handful of something like...

const OMIT_ICON: Option<fn(i32) -> bool> = None;

Also, how much control do you have for the bounds? Do you need to stick to Fn() or could you use your own trait?

Just a few, but having to pass OMIT_ICON every single time would make almost useless the concept of optional parameters (component properties in Leptos). I better like, if possible, to solve this on the called function, not at the site of calling.

I have total control over the type, it does not even need to be an Option, just something that can represent an "optional invokation". The important part is that the argument I pass to the function can be invoked multiple times (as a Fn) to produce the result, i.e. an impl IntoView.

So, I take it, you want to not mention it in the view! { .. } at all.

To lob another idea over the fence, does this work (I don't know Leptos):

const OMIT_ICON: Option<...> = None;

#[component]
fn Button<F, IV>(
    #[prop(default = OMIT_ICON)] icon: Option<F>,
    // other "props", not important right now
) -> impl IntoView
where
    F: Fn() -> IV + Send + Sync + Clone + 'static,
    IV: IntoView + 'static,
{
    // Button implementation here.
}

If it fails, can you share why? (I'm sort of grasping since I don't know what all these macros are actually producing. I have a more convoluted idea but it has some of the same weaknesses as this idea, so I'll stash it for now.)


It feels like the ideal solution would be defaulted parameters, but they don't work great in general... and it is now a future incompatibility warning to use them on functions specifically, instead of just a warn-by-default lint. Personally I feel this is a bit aggressive, but as far as this topic goes it's probably a dead-end anyway -- you need to use a turbofish to make them useful on functions, which presumably you can't get Leptos to do (or if you can it requires cruft on view!{} which you don't want).

If you have a trait Invoke (essentially Fn), does it make sense to implement it for () (or a type equivalent to it)? Is there a default "do nothing" function you can implement?

Alternatively, can you move the Option into the return value? Returning None means "do nothing" while returning Some(x) means "do what you would have done with x."

for a standalone None, the type parameter always needs to be inferred. personally, I would prefer to use some custom sentinel value instead of None if there's no context for type inference.

that said, one trick to make such inference succeed is (ab-)using the single applicable implementation rule, but you do need to change the signature of take_optional_f() a bit:

1 Like

Thank you for the suggestion. Unfortunately my naive approach did not work: given that IntoView is implemented for () (it is the result of an empty view macro call: view! {}) I tried:

const OMIT_ICON: Option<fn() -> ()> = None;

that fails with:

error[E0308]: mismatched types
  --> app/src/components/button.rs:14:1
   |
14 | #[component]
   | ^^^^^^^^^^^^ expected `Option<F>`, found `Option<fn()>`
15 | pub fn Button<F, IV>(
   |               -- expected this type parameter
   |
   = note: expected enum `std::option::Option<F>`
              found enum `std::option::Option<fn()>`

This is very much like what I had in mind: using a wrapper type and a trait to "hide" the generic signature. Now I have to see how it works in leptos but from a more generic point of view I am really happy with how it works. Thank you for showing me how to do it.

More or less what I was afraid of. Do I understand correctly that @nerditation's suggestion has solved your problem?

@quinedot yes and no. @nerditation showed what I was asking, i.e., how to use a wrapper/marker type and a trait to erase the generic argument and that's something I'll use ina couple of places in a library to make it easier for consumers to call functions without having to specify a type when using None.

Unfortunately even that does not work in Leptos, using just None, the marker type or even None::<Infallible> as the default value for a prop fails with the exact same error I showed above. In the end I'll probably just split components with optional properties into a set with/without the property (Button, IconButton, ...) and provide a OMIT_xxx const to use where I want to keep the optional prop.

1 Like