Difference in function definitions that take a closure

Hi, new to Rust here.

Learning the part about passing functions as arguments for other functions and running some examples I see that can be more ways than one to define a function signature that takes a closure as a parameter.

Example where impl keyword can be used:

fn takes_closure<T>(f: impl Fn() -> T) {
    f();
}

Example with generic parameters with trait bounds:

fn takes_closure<F, T>(f: F)
where
    F: Fn() -> T
{
    f();
}

Example with trait objects using Box smart pointer:

fn takes_closure<T>(f: Box<dyn Fn() -> T>) {
    f();
}

So my question is what are the advantages of one way over the other, and what are the use cases for different ways of defining function that takes other function as a parameter?

Reading trough tutorials and code examples on doc.rust-lang I noticed that way from Example #2 is preferred, but maybe I'm wrong about it.

impl Trait in argument position is functionally identical to a generic parameter. It used to be that you couldn't turbofish a function at all that used APIT, but now you can for the explicit generic parameters without specifying the implied generic parameter from the impl Trait.

So I much prefer using impl Trait for types that I usually can't turbofish anyway, since it makes things cleaner.

Take this function, for example:

fn foo<T, F: Fn() -> T>(f: F) { … }

If I want to annotate the T explicitly, I have to write something like this:

foo::<u64, _>(|| 3);

But if I choose instead to define the function using this:

fn bar<T>(f: impl Fn() -> T) { … }

Then I don't have to (and can't) put anything for the type of the closure, and can just write

bar::<u64>(|| 3);

Which is way nicer, since I can never actually write out the type of a function anyway. (I could pass along a generic, but I almost never need to do that because I have an instance I can just pass that makes it infer it correctly anyway.)

And just in general, it's pretty common with closures and iterators that you don't really care what exact type you have, and you don't need to mention it again later, in which case mixing explicit generics and argument-position impl trait can make it nicer to read the signatures.

For example, I find that a definition like this reads really well:

fn merge<T: Ord>(left: impl Iterator<Item = T>, right: impl Iterator<Item = T>) -> impl Iterator<Item = T>;`

I think of that as "being generic over T, but taking two iterators", even though it's generic over three different things in the implementation details.

There are other ways that could be written, like

fn merge<L: Iterator, R: Iterator<Item = L::Item>>(left: L, right: R) -> impl Iterator<Item = L::Item>

But that just reads so much worse to me.

7 Likes

Thanks scottmcm.
I see, first two examples in my question is same thing really, difference being that using impl Trait can be more readable.

What about using dynamic trait objects referenced with some kind of a pointer?
Like for example:

fn takes_closure<T>(f: Box<dyn Fn() -> T>);

I get what trait objects are for but why put argument like this in function signature?
I saw that in Rust book.
Why using trait object at all in this case?

Whether you use dynamic dispatch depends entirely on whether you need dynamic dispatch. Usually, one would prefer to use statically-typed, monomorphized closures, because they can be easier to optimize for the compiler, and trait objects can be more inconvenient to handle in some cases due to them being dynamically-sized.

However, sometimes you need dynamic dispatch, e.g. a router for an HTTP service must store the handlers associated with each route in some sort of a collection, so their type must be uniform. That's usually where you will see Box<dyn Fn>s around. As the argument of a higher-order function, it's pretty rare and non-idiomatic.

3 Likes

?

Argument Position Impl Trait.

The other related acronyms:

  • RPIT - Return Position Impl Trait
  • TAIT - Type Alias Impl Trait
  • ITIT - Impl Trait In Traits
    • (I just made this one up, I haven't seen it used)

(I'm talking about dyn Trait in general, not Fn* traits specifically)

In the book, probably solely because it was illustrating the usage of trait objects.

In a library, it's generally preferred to take generics, because then the caller gets to choose whether to provide some concrete type or a &dyn Trait at the call site, depending on what they want.

However, even when you don't strictly require dynamic dispatch, it can be desirable to still use trait objects. This is generally referred to as "polymorphization" (the opposite of "monomorphization"); by only having a single codepath, the compiler doesn't have to monomorphize a bunch of copies everywhere, improving compile times some, and reducing the size of the generated machine code. Counterintuitively, this can sometimes lead to performance improvements because of various 2nd-order effects like cache coherency. (AIUI, this primarily occurs when the amount of work done "behind" the dynamic dispatch is small relative to the work done by the function.) In theory LLVM can do "outlining" passes to get this benefit (and MIR optimizations are starting to do some polymorphization in simple cases), but the effects are typically so unpredictable that it's a matter of sitting down with a profiler and measuring the impact of changes you do. (For the compile time benefits, though, consider if the code path isn't likely to be hot if you could use dyn dispatch. You can always fairly easily monomorphize it later by switching dyn to impl.)

At a crate API level, though, even if doing so internally, it's typical to take impl Trait at the API boundary as a thin dispatch layer to your internal &dyn Trait code. (However, since this can introduce extra indirections, it can sometimes be preferred to just directly take &dyn Trait instead, since the caller can do the coercion as easily as you can. &impl ?Sized + Trait could work but be pretty unnecessary, perhaps even hurting type inference.)

5 Likes

… you’re funny and thank you. You may have just started a __IT trend.

That is really insightful, thanks for such an under the hood explanation. None of those things would never occur to me, only that I knew about monomorphization/polymorphization with regards to trait/trait objects.

So if I want to store functions in a collection I need dyn, but what if I want to store closures that accept no parameters and can return different types as a result?
Using a tuple struct as a store for example.
This obviously wont work:

struct Sf<T>(Vec<Box<dyn Fn() -> T>>);

I need to specify T and can only have functions that return u32 or &str or some other type.
How can I have Vec of functions in which I can store functions that return any type?

The first question then becomes, how do you plan on calling the functions? Whomever calls any given function needs to know what it returns, so that it can receive the value correctly.

The simplest approach is "just don't do that;" store functions returning nothing, and to bridge functions which return values, define a closure along the lines of || drop(f()) or || { f(); }.

The second approach is to lean into the dynamic dispatch; this is what #[async_trait] does. If all values that you want to return implement some Trait, you can return dyn Trait. However, you still need to box the trait object somehow, using Box<dyn Trait> or whichever ?Sized-accepting container is most appropriate.

The third approach is the most difficult: maintain some kind of dynamic type map. The most trivial is HashMap<TypeId, Box<dyn Any>> with a getter along the lines of

fn get<T: 'static>(&self) -> Option<&T> {
    let any: &Box<dyn Any> = self.inner.get(TypeId::of::<T>());
    any.downcast_ref::<T>(&**any)
}

Typically, if you can use an earlier solution, do. The type map is only required for doing advanced things like tracing-subscriber or http's extension storages or bevy's ECS storage, where you're abandoning the Rust type system to do things manually.

3 Likes

The main question is, how will you use this Vec? What are you allowed to do with functions[i] (or even with functions[i]())?
If the answer is "to check the type at runtime and then do whatever this check allows" (unlikely, but possible) - you want these functions to return Box<dyn Any>.
If the answer is "whatever is possible statically" - you probably want not a Vec, but something like typemap.

2 Likes

Cerber-Ursi thanks, typemap looks really flexible and powerful I plan to check it out in more detail eventually.

CAD97, yes you are right, the main question would be how to call those functions and use returned values.

I really like the bridged solution although for more well defined requirements I would aim to use implemented boxed dyn Trait.

Third approach is right now to advanced for me, I will need to study it more.
For example, I see in the documentation that

pub fn downcast_ref<T>(&self) -> Option<&T>

takes only &self as a parameter but you used &self and $**any when calling it in this line:

any.downcast_ref::<T>(&**any)

Also why not just return

let any: Box<dyn Any>

instead of reference to Box?

let any: &Box<dyn Any>

That's just me making a silly typo from writing this without a compiler checking my work :upside_down_face:

1 Like

I was already starting to think that this is another one of Rust's hidden features :).

Thanks to all for detailed answers it was really helpfull, I learned lot's of new things about the language.

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.