Incorrect/Illogical behavior for deducing fn

pub fn foo<F2, F3>(
    show_cpy_dlg_cb: F3,//if I change it to F2 it won't compile
    hide_cpy_dlg_cb: F2, 
) -> anyhow::Result<()>
where
    F2: FnOnce(&mut Cursive) + 'static + std::marker::Send + std::marker::Copy,
    F3: FnOnce(&mut Cursive) + 'static + std::marker::Send + std::marker::Copy,

Here are the prototypes for those functions:

pub fn show_cpy_dlg(s: &mut Cursive)
pub fn hide_cpy_dlg(s: &mut Cursive)

Why is rust behaving in such illogical way? Those functions have identical prototypes, so why do I need to specify different trait for each of them?!?!?!?

As I understand it, functions each get their own unique type similar to how closures work. These special function types are called "fn items". This helps the compiler do a bunch of optimizations safely, but occasionally causes confusing type errors. Fn items can always be coerced to function pointers, which behave how you'd expect. In many places the coercion is automatic, but type inference gets in the way of the coercion here.

You can coerce the functions to function pointers explicitly if you really want to be able to pass them both as the same type with as fn(&mut Cursive)

3 Likes

Hi and thank you for your reply. Could you please show me example of how to do that coersion?

pub struct Cursive;

pub fn foo<F2>(
    show_cpy_dlg_cb: F2,
    hide_cpy_dlg_cb: F2,
) -> anyhow::Result<()>
where
    F2: FnOnce(&mut Cursive) + 'static + std::marker::Send + std::marker::Copy,
{
    todo!()
}

pub fn show_cpy_dlg(s: &mut Cursive) {}
pub fn hide_cpy_dlg(s: &mut Cursive) {}

fn bar() {
    foo(show_cpy_dlg as fn(&mut Cursive), hide_cpy_dlg);
    // Or specify the type of the type parameter with the turbofish operator
    foo::<fn(&mut Cursive)>(show_cpy_dlg, hide_cpy_dlg);
}
3 Likes

Thank you.

Yes, this issue has also recently been mentioned here in the thread "What is the exact type of a function?".

It was a surprise for me as well.

1 Like

This code now looks a bit weird to me. The only way to call this function now is either with the same closure or function twice[1] or with a function pointer. Other options are practically nonexistent since Copy prevents boxed trait objects (FnOnce or similar) or mutably borrowed trait objects, and &dyn Fn(...) is mostly ruled out by the 'static.

So either, the separate parameter F3 should be kept, or the function can be made fully monomorphic to always expect function pointers. (The choice depends on whether capturing closures [capturing Copyable data] should be allowed, and - more importantly - on whether the monomorphization for each given pair of function types, and avoiding dynamic function calls, is seen as a relevant performance benefit.) As a positive side effect, either approach would make the calls-site straightforward again.


  1. which I assume is not supposed to be a common use case here, but maybe I'm wrong? ↩︎

4 Likes

What do you mean by the only option is to call it twice? If I call show_cpy_dlg_cb, the show fn will get called and not twice nor will the hide fn get called either.

You're missing the “or with a function pointer” that ends that sentence in that quote. (Just making sure you didn't overlook that.)

In case it was too sloppily formulated: “called with the same ... twice” is supposed to mean “called with two copies of the same ... as the two arguments”.

The code from @semicoleon by the way demonstrates the approach of using function pointers, so the “same closure twice” doesn't apply.

But it is not called with two copies of the same. Those are different callbacks, the "type" of them is identical.

That was the whole point of this exercise. Function pointers, as @semicoleon explained

EDIT
OK. I see what you mean when you are saying two copies... Yes, the only sensible use of it is by using fn pointers

Maybe focus on my conclusion first, if I didn’t present the argument (leading to that conclusion) well enough:

The point is that the use-case of using function pointers is (essentially) the only use-case left (except for those where two copies of the same function/closure are passed). So if only function-pointers are to be used anyways, the signature could as well be written

pub fn foo<F2>(
    show_cpy_dlg_cb: fn(&mut Cursive),
    hide_cpy_dlg_cb: fn(&mut Cursive),
) -> anyhow::Result<()>

and now this function can even be called via foo(show_cpy_dlg, hide_cpy_dlg) again.

3 Likes

Yep, you are correct. I've added EDIT to my last post.
Thanks!

1 Like

To further explain the argument, in case the rest was not clear enough before, either: For closures and fn items, if the types are the same then the closure/function is the same, too, which is a not too useful case. The only implementors of FnOnce besides closures and fn items are

  • function pointers
  • dyn FnOnce/dyn FnMut/dyn Fn trait objects
  • Box<T>, &mut T, &T for T being any of these cases (possibly recursively)

The additional bounds rule out most of those. The Copy bound rules out Box<T> and &mut T, and the 'static bound rules out (most reasonable cases) of &T, too.The trait objects can never be passed alone, so by Box<T>, &mut T, &T being eliminated, they are out of the equation, too; thus only function pointers are left.

(And technically something like &'static dyn Fn(…) would work, too, but I would have never seen such a thing before.)

3 Likes