Question about closures

While experimenting with closures I've come up with the following code:

fn m3(a: bool) -> impl Fn(i32) -> i32{
    if (a){
        |x| x+1
    }
    else {
        |x| x
    }
}

Considering that both of these closures are essentially different types I'm not really sure why does this work. After adding return keyword before the closures:

fn m3(a: bool) -> impl Fn(i32) -> i32{
    if (a){
        return |x| x+1;
    }
    else {
        return |x| x;
    }
}

The result is different and Rust compiler responds with this:

error[E0308]: mismatched types
  --> src\main.rs:27:38
   |
27 |   fn m3(a: bool) -> impl Fn(i32) -> i32{
   |  ______________________________________^
28 | |     if (a){
29 | |         return |x| x+1;
30 | |     }
...  |
33 | |     }
34 | | }
   | |_^ expected closure, found fn pointer
   |
   = note: expected closure `[closure@src\main.rs:29:16: 29:19]`
           found fn pointer `fn(i32) -> i32`

Why does Rust accept first version of my code?

2 Likes

I think this is a case of weird type inference nonsense.

It only works in the first case because your closures don't capture any environment so they get coerced into function pointers.

When you explicitly return, the compiler takes the exact type of the returned value (the opaque closure type) and decides that's what the function returns, so the two different types can't be unified into function pointers.

When you use implicit return the compiler doesn't have a fixed type for the return type, so the inference machinery kicks in and coerces them to function pointers.

You can fix the return version by adding an explicit function pointer cast

fn  m3(a: bool) -> impl Fn(i32) -> i32{
    if (a){
        return (|x| x+1) as fn(_) -> _;
    }
    else {
        return |x| x;
    }
}
4 Likes

This also won't work:

fn m3(a: bool) -> impl Fn(i32) -> i32 {
    let retval;
    if a {
        retval = |x| x+1;
    }
    else {
        retval = |x| x;
    }
    retval
}

(Playground)

But this does:

fn m3(a: bool) -> impl Fn(i32) -> i32 {
    let retval: fn(i32) -> i32;
    if a {
        retval = |x| x+1;
    }
    else {
        retval = |x| x;
    }
    retval
}

(Playground)

What I don't understand is why type inference coerces into function pointers in case of

    if (a){
        |x| x+1
    }
    else {
        |x| x
    }

but fails with:

    let retval;
    if a {
        retval = |x| x+1;
    }
    else {
        retval = |x| x;
    }

:man_shrugging:

Two more variants that work (and which also work when you can't coerce into a function pointer):

fn _foo(a: bool) -> impl Fn(i32) -> i32 {
    move |x| {
        if a {
            x+1
        } else {
            x
        }
    }
}

fn _bar(a: bool) -> Box<dyn Fn(i32) -> i32> {
    if a {
        Box::new(|x| x+1)
    } else {
        Box::new(|x| x)
    }
}

(Playground)

I wonder which of these versions is better regarding runtime behavior.

In the case of both return and retval = there's an ordering of operations where the "first" operation gets to decide the type.

When you use the whole if as an expression both operations (returning each branch's closure) are happening "at the same time" from the perspective of the type checker since they're all part of the same expression.

3 Likes

I would generally expect _foo to be faster since it's a single closure type. It can easily be inlined that way. Boxing and returning function pointers both make optimizations harder.

1 Like

Why do you have to perform a cast in the first return but not in the second one?

The first return forces the type checker to choose fn(i32) -> i32 as the return type for the whole function. So the second return gets coerced because it can't choose the return type for the function anymore, but there is a valid coercion that would make the types compatible.

For the sake of experimenting, when the bool is always known at compile time, you could also use const generics:

fn g<const A: bool>(x: i32) -> i32 {
    if A {
        x+1
    } else {
        x
    }
}

fn main() {
    let func1 = g::<false>;
    let func2 = g::<true>;
    println!("{}", func1(4));
    println!("{}", func2(8));
}

(Playground)

But not sure if that's idiomatic and/or of much practical use.

And also after I save the value that the function returns it will be coerced back from function pointer to Fn right?

I'm not entirely sure what you mean, but once a value is cast to a function pointer you can't recover the information about where it came from, no.

If you mean "can I pass a function pointer to something expecting a Fn?" then the answer is yes. Function pointers implement the appropriate Fn traits.

1 Like

Here's an interesting way to write that one that doesn't end up at fn:

fn m3_as_actual_closure(a: bool) -> impl Fn(i32) -> i32 {
    fn add(y: i32) -> impl Fn(i32) -> i32 {
        move |x| x + y
    }

    if a {
        add(1)
    } else {
        add(0)
    }
}

And you can tell it's not using function pointers by checking the size -- it's a closure with i32 state:

[src/main.rs:24] size_of_val(&m3(true)) = 8
[src/main.rs:25] size_of_val(&m3_as_actual_closure(true)) = 4

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=eac39ec3ecbc1effb0568d9246a68be5

3 Likes

So essentially when there is an implicit return Rust tries to coerce returned expressions into universal type of some sort because the type of the function is not fixed and when there is an explicit return Rust takes opaque type and sets it to return type of the function which is not compatible with other return types right?

I suspect it's actually the if expression that forces both branches to coerce to a common type (a fn pointer in this case), in a way that return + RPIT doesn't. To illustrate, this compiles (Rust Playground):

fn m3(a: bool) -> impl Fn(i32) -> i32 {
    let retval = if a { |x| x + 1 } else { |x| x };
    return retval;
}
6 Likes

Huh. This is definitely cursed Rust iceberg meme material.

That would make sense.

But what's going on here? Why Rust doesn't say mismatched types when compiling this?

add creates a single closure type which captures the i32 value y.

The closure type doesn't change with respect to the inputs to add.

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.