What is the idiomatic way in rust for a function accepts a closure as argument or return a closure?

Viewed 267

What is the idiomatic way in rust for a function accepts a closure as argument or return a closure?

I see it can be done in at least the below 3 ways:

// 1
pub fn run_with_envs_guard1(envs: &HashMap<&str, &str>, f: &dyn FnOnce()) {}

// 2
pub fn run_with_envs_guard2(envs: &HashMap<&str, &str>, f: Box<dyn FnOnce()>) {}

// 3
pub fn run_with_envs_guard3<F: FnOnce()>(envs: &HashMap<&str, &str>, f: F) {}

Are there really some differences among these 3 ways? If yes, pls help to clarify, and which way is more idiomatic i should choose?

I am learning rust still, sorry if all the above ways are some bad/strange things.


Maybe a more specific question, why in way 1 and 2 i need the dyn keyword, but in 3 i don't, from my understanding, these all need dynamic dispatching, is it? as the actual function cannot be determined in compiling time

2 Answers

Abdul answers the first half of your question (and I agree completely with what he said), so I'll take a stab at the second half.

If you want to return a closure from a function, you can't return a type parameter, because that implies that you're returning an instance of any FnOnce, at the caller's choice. You can't return a &FnOnce, because you (usually) need to pass ownership to the caller. You could make it work with Box<FnOnce>, but that tends to just be clunky to work with. When returning closures from functions, I'm partial to the impl trait syntax.

pub fn test() -> impl FnOnce() {
  || { println!("It worked!") }
}

In argument position, writing impl FnOnce() as the type of something is equivalent to defining a type argument, as Abdul did in his answer. However, in return position, it's an entirely new feature that returns an opaque value. It says "I'm returning an FnOnce, and I'm not telling you which one it is". It's the same concept as a trait object, but without the overhead of throwing it in a box.


Responding to your edit

i don't, from my understanding, these all need dynamic dispatching, is it? as the actual function cannot be determined in compiling time

This is actually not necessarily true. If you see the dyn keyword, then there's definitely a dynamic (runtime) dispatch happening. To understand your other example, though, let's consider a simple trait that doesn't have the baggage of FnOnce.

pub trait MyTrait {}

struct Foo;
struct Bar;

impl MyTrait for Foo {}
impl MyTrait for Bar {}

pub fn example<T: MyTrait>(_arg: T) {
  println!("It works!");
}

fn main() {
  example(Foo);
  example(Bar);
}

I claim there's no dynamic dispatch happening here. Rust monomorphizes functions with type parameters. That means that example is like a template function in C++. Every instantiation of it will end up being a separate function. So, really, during Rust's compilation, this will end up being more like

struct Foo;
struct Bar;

pub fn example1(_arg: Foo) {
  println!("It works!");
}

pub fn example2(_arg: Foo) {
  println!("It works!");
}

fn main() {
  example1(Foo);
  example2(Bar);
}

Two unrelated functions that happen to do something similar. Rust resolves all of the linkage statically, so there's no dispatch happening at runtime. In fact, we can prove it. Take the code I just posted above and compile it with debugging symbols on (rustc -g filename.rs). Then use a tool like nm (available on most Linux machines by default) to list all of the symbols in the linker table. Assuming you didn't turn any optimizations on, you should see two example functions. This is what they look like in my linker

0000000000005340 t _ZN10code7example17h46383f9ad372dc94E
00000000000053a0 t _ZN10code7example17h97b400359a146fcaE

or, with nm -C to demangle the function names

0000000000005340 t code::example
00000000000053a0 t code::example

Two different functions, each of which takes concrete arguments of specific types.

Your proposed FnOnce would work the same way.

pub fn run_with_envs_guard3<F: FnOnce()>(envs: &HashMap<&str, &str>, f: F) {}

Every closure in Rust has a distinct type, so every time this function is called, a new version of run_with_envs_guard3 will get made, specifically for that closure. That new function will know exactly what to do for the closure you just gave it. In 99% of cases, if you have optimizations turned on, these made-up local functions will get inlined and optimized out, so no harm done. But there's no dynamic dispatch here.

In the other two examples, we have a dyn FnOnce, which is more like what you'd expect coming from a traditionally object-oriented language. dyn FnOnce contains a dynamic pointer to some function somewhere that will be dispatched at runtime, the way you'd expect.

I would prefer the third one. Because the rust documentation suggest to Use FnOnce as a bound when you want to accept a parameter of function-like type and only need to call it once.

pub fn run_with_envs_guard3<F: FnOnce()>(envs: &HashMap<&str, &str>, f: F) {}

This means that the F to be bound by FnOnce(ie, F must implement FnOnce)

Related