Ergonomics question. Getting around "cannot infer type for type parameter" for optional arg

Hi Everybody,

I have a function that takes an optional argument with a... shall I say... specific, type.

struct MyStruct{}

impl MyStruct {
    pub fn do_something <
        ClosureResultT: PartialOrd + Copy + Default + From<f64>,
        MyClosureWithLotsOfArgs: Fn(
                usize, usize,
                &[i16; 4]) -> Option<ClosureResultT>,
            >(&self, my_closure : &Option<MyClosureWithLotsOfArgs>) {

    }
}

fn main() {
    let my_struct = MyStruct{};
    
    my_struct.do_something(&None);
}

But 90% of callers want to just call do_something(&None).

So is there a way around having to specify something like

let my_none : Option<Fn(usize, usize, &[i16; 4]) -> Option<f64>> = None;
do_something(&my_none);

Not to mention that doesn't compile either... To actually get it to compile, I need to define the closure type in the calling function's (or impl's) type definitions.

Anyway. This is pretty hideous, and I was wondering if anyone else has come up with an elegant solution for this kind of situation.

I should also mention, that I understand the compiler needs to know the type of the parameter to generate code for the side of the branch when the Option isn't None, but I'd be happy with a solution that compile-time specialized the function into a version that totally omitted the non-None branches. But I couldn't figure out the right way to do that without some really ugly macros.

Thanks in advance.

Can you provide a version that has the imports that you use?

Slightly modified version on the playground, for future readers:

use core::fmt::Display;
use core::ops::AddAssign;
use core::ops::Div;
use core::ops::Sub;

pub fn do_something<
    ClosureResultT: PartialOrd
        + Copy
        + Display
        + Default
        + Sub<Output = ClosureResultT>
        + Div<Output = ClosureResultT>
        + AddAssign
        + From<f64>,
    MyClosureWithLotsOfArgs: Fn(usize, usize, &[i16; 4]) -> Option<ClosureResultT>,
>(
    my_closure: &Option<MyClosureWithLotsOfArgs>,
) {
    todo!()
}

Changes:

  • dropped Zero and Bounded from ClosureResultT bounds (the exact bounds seems to be not relevant to the question anyway);
  • added necessary imports from std;
  • dropped &self argument (to format as a free function);
  • run rustfmt.
2 Likes

You beat me to it. My playgrounded version is in the original post, now showing only the "cannot infer type for type parameter" error.

Thanks!

Playground for a solution to this portion of the question.

1 Like

Thank you! My mistake was using Fn instead of fn in my type definition for my_none. Thank you!

Still hoping there is a way to compile-time specialize the function for the absence of an argument.

But I wonder if the way you've made the argument const results in the compiler potentially taking the liberty. Still make the call ugly, but perhaps just as efficient. Haven't look at the ASM yst.

Thanks again!

1 Like

So I've been playing with this a bit more, and I was getting more comfortable with the not-so-ugly solution @quinedot proposed of having a const declared that casual users could just pass in without thinking about it.

And all seemed well in the world until I tried to move the declaration of the const into a library. Now, presumably because of dynamic linking, I'm getting cannot infer type errors.

Equally troubling, this also hints that the compiler won't be able to optimize away the const if it's part of a separate binary object. :frowning:

I am conceding defeat. :frowning:

I'll just write a wrapper function without the optional arg. Then the caller can call the appropriate entry point depending on the functionality they want. Some people might say that's cleaner anyway.

Thanks for all the replies.

Instead of making the closure optional, you could also provide a nop or identity function to use as the argument when users don’t need any special behavior— Then your wrapper function becomes a trivial 1-line convenience function.

Thanks for the reply. I tried that, but the trouble is that the closure's return type is a generic. So the no-op function needs to be specialized by the compiler, even though it ultimately doesn't matter.

So in practice I ended up in exactly the same boat of "cannot infer type" errors.

But if the closure’s return type is generic, then it’s caller-specified, and your example function always returns (). What is the effective return type of the closure if the user passes in None?

It works great when it's all in one file. But as soon as the no-op closure is imported from a library (the same library that contains the function I'm passing the closure to), the calling code won't compile because it complains the type isn't known.

That sounds odd; how did you define the nop function? Not knowing how it’s used, I would expect it to be something like this:

pub fn nop(_:usize, _:usize, _:&[i16; 4])->Option<f64> { None }

Because the closure specifies its own return type, the nop you supply needs to use a concrete return type to avoid type inference failures; there’s no other place that information could come from.

1 Like

Like so?

2 Likes
None::<SomeType>

is a valid syntax. None::<fn(usize, usize, &[i16; 4]) -> Option<f64>> is awful long to type, but you can make an alias:

type Callback = fn(usize, usize, &[i16; 4]) -> Option<f64>;

and use None::<Callback>.

You can also define a constant for this:

const NO_CALLBACK: &Option<Callback> = &None;

and use NO_CALLBACK instead.

1 Like

To repeat my pattern presented in this answer, a zero-sized option using an uninhabited function type appears to be an elegant solution to me. This will guaranteed not generate any non-None branches anymore.

#[inline]
fn none() -> Option<impl Fn(usize, usize, &[i16; 4]) -> Option<f64>> {
    enum Void {}
    None::<Void>.map(|void| move |_, _, _: &_| match void {})
}

(playground)


Edit 2022: This code will need slight modification to get the same zero-sized option behavior in edition 2021.

2 Likes

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.