Question about closure and return keyword vs without return keyword

I have two similar code snippets, one uses return keyword and other doesn't,
the one with return statement gives compiler error "expected closure" found fn pointer.

while one without return keyword compiles. why the difference ?

fn lam_error(arg : Vec<String>, k : i32) -> impl Fn(Vec<String>) {
    if k == 1 {
       return |arg| {
            println!("j {:?}", arg);
        };
    } else {
        return |arg : Vec<String>| {
            println!("j1 {}", arg.len());
        };
    }
    
}

fn lam_no_error(arg : Vec<String>, k : i32) -> impl Fn(Vec<String>) {
    if k == 1 {
       |arg| {
            println!("j {:?}", arg);
        }
    } else {
        |arg : Vec<String>| {
            println!("j1 {}", arg.len());
        }
    }
    
}
1 Like

Interesting. Apparently the return makes the compiler decide the closure type is the opaque type you're going to return, whereas without it the compiler managed to unify the types of the two branches (by coerce both closures to fn(Vec<String>)).

You could file an issue about it.

3 Likes

Interesting. I'm not completely sure I know what's happening, but I'll give it a shot. I think the problem is that the first function makes each closure be evaluated separately, not really the return keyword per say. This version actually works:

fn lam_error(k : i32) -> impl Fn(Vec<String>) {
    return if k == 1 {
        |arg: Vec<String>| {
            println!("j {:?}", arg);
        }
    } else {
        |arg: Vec<String>| {
            println!("j1 {}", arg.len());
        }
    }
}

Closures are kind of funny. Even though both closures implement the Fn(Vec<String>) trait, each instance is its own unique type. The return keyword basically says "break normal control flow early with this value." Each closure is considered individually, so the compiler has to coerce them into the same type. The next closest thing is a function pointer. Because both closures have the same arguments and return type, they actually are the same type fn.

Yeah, that's a bit confusing. The trait Fn(Vec<String>) is how you describe a closure, and the type fn(Vec<String>) is a function pointer. Two different closures can never be the same type, but using them is a little bit more flexible. Basically, if you're okay with using a function pointer, then this works:

fn lam_error(arg: Vec<String>,k : i32) -> fn(Vec<String>) {
    if k == 1 {
        return |arg:Vec<String>| {
            println!("j {:?}", arg);
        }
    } else {
        return |arg:Vec<String>| {
            println!("j1 {}", arg.len());
        }
    }
}

Short version, something about having both closures in a single expression lets the compiler unify the types as a closure. Breaking them apart into separate return expressions makes the type evaluated separately for each one, then they need to be coerced into function pointers as a second step to unify the types.

1 Like

Oh, also, you may be misunderstanding closure syntax. Those arg function parameters aren't actually being used. You really just need this:

fn lam_no_error(k : i32) -> impl Fn(Vec<String>) {
    if k == 1 {
       |arg| {
            println!("j {:?}", arg);
        }
    } else {
        |arg| {
            println!("j1 {}", arg.len());
        }
    }
}

Think of the variables between the vertical pipes as function parameters that you can skip type annotations if you want. Example:

fn main() {
    let closure = |some, parameters| println!("{some} {}.", parameters * 7);
    closure("The answer to life, the universe, and everything is", 6);
}
1 Like

That might be it.

In the no_error (no return) case, the two closures are first "unified" against each other (as the two branches of an if-expression) resulting in coercion into a fn pointer, which is then "unified" against return type which just sets the return type to that fn pointer.

In the error (return) case, the first closure is "unified" against the return type (because it is an argument of a return statement) which sets the return type to type of that closure, then the second closure is "unified" against the return type (which has been already set to the first closure type), which fails (the two closures have different types and adding a coercion is probably not considered by compiler here)

2 Likes

Thanks. Yes, FP style expression evaluation is unifying types. That's not happening with separate returns.

Thanks, Yes.

It is random sample which I was playing with and stumbled on diverse compilation. I understand that args argument was not used. I should have removed it while posting.

Thanks!

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.