Is there ever a use for non-thread safe `Fn` bounds?

I'm trying to understand the differences between Swift and Rust closures, and while I think Swift is definitely missing something similar to FnOnce, I'm failing to see the great utility of Fn outside of threading (for which Swift has @Sendable closures).

What use-cases are there for a plain F: Fn bound? That is, a Fn closure bound that is not 'static, Send nor Sync.

In particular, I'm interested in examples of fn use_fn_closure<F: Fn>(closure: F) (choose your own argument & return types) that does something interesting with the closure, and where the bound could not be relaxed to F: FnMut.

1 Like

I took a look at the standard library and found such a bound used only on std::fmt::FormatterFn, which I guess is somewhat useful.

Still interested in other examples, especially if you have an example where the closure is not stored in a struct, or at least where the struct is not parametrized by the lifetime of the closure?

Your FormatterFn link is broken, there's no such thing as std::fmt::FormatterFn, and I don't see anything relevant in std::fmt::Formatter. What were you meaning to link to there?

Apologies, I've fixed the link now.

I think that's the reason the use Fn over FnMut anyway (without threading). If you never store the Fn in a struct, like calling Iterator::map or construct a Vec of dyn Fn, then all your usage of f would never overlap, then you can upgrade it to FnMut.

In general, off the top of my head, the main thing that Fn (without Sync) allows for is

  • re-entrant calls
  • calls behind a shared-ownedship abstraction, like Rc

The latter could alternatively still be achieved with a RefCell wrapper; though more precisely with a RefCell you can literally implement Fn bounds in terms of FnMut ones, so that’s a general “solution”. This comes with slight performance overhead as the RefCell dynamically checks exclusive access, and it would lead to failure (typically panic, if you use the panicking API of RefCell) precisely in the case of a re-entrant call again, which leads us back to the first bullet point.

7 Likes

If you lock something outside the closure, you might want to capture a LockGuard in the closure, but the guard isn't Send.

2 Likes

Regarding re-entrant examples… well, I suppose those are not entirely trivial. Here’s a somewhat nonsensical one, though I wouldn’t be surprised if there are cases where you can structure some actual complicated recursive practical/useful stuff in ways that has similar structure:

fn use_fn_closure<F>(f: F)
where
    F: Fn(Option<&dyn Fn()>),
{
    f(Some(&|| {}));
    f(Some(&|| {
        f(None);
    }));
    f(Some(&|| {
        f(None);
        f(None)
    }));
}

fn main() {
    use_fn_closure(|x| match x {
        None => println!("called…"),
        Some(f) => {
            println!("called with arg:");
            f();
            println!("end of first callback");
            f();
            println!("end of second callback\n");
        }
    })
}

Rust Playground


Regarding other ways… as already mentioned Rc could be involved; furthermore, the Fn could be put into a thread-local variable and called, something that doesn’t work with FnMut, and something that’s also even easier to get re-entrant as you wouldn’t have to pass the function to itself explicitly (as the code above, where trait objects were used to make that possible).

More often than not however, the re-entrancy isn’t super useful in and by itself, but rather the fact that it’s hard for the compiler to rule out re-entrant calls in certain situations – especially when you’re providing some API for arbitrary user code – so FnMut may be unusable for that reason alone. (And thread-locals in particular commonly are really just one such case:[1])


Furthermore, the linked example of FormatterFn shows another use-case: You can use a F: Fn(…) -> … in order to create a Wrapped<F> implementing some single-method fn foo(&self, …) -> … method. The &self means that the same thing wouldn’t work with FnMut.


  1. LocalKey::with only provides shared immutable reference access to the thread local because of the possibility of re-entrant calls to LocalKey::with within the callback. And this is not because such re-entrant calls are super useful all the time, but because there’s just no way to prevent users from doing it… besides run-time checks, which you can opt into by using RefCell after all. ↩︎

1 Like

Send/Sync aside, the use case for Fn over the alternatives are

  • You need to call it more than once (can't use FnOnce)
  • You won't have exclusive access to it when you use it (can't use FnMut)
    • (and don't want to unnecessarily pay synchronization costs)
    • E.g. you're storing it in Self and need to use it in a &self method
    • E.g. you need to call it from multiple closures of your own

The most common use of closure bounds (certainly in std but probably everywhere) are accepting closures that you're going to use immediately in the function body and then discard, so the latter cases are pretty rare and you will generally see FnOnce or FnMut.

The only purpose of FormatterFn is to store the closure and call it from &self-receiving trait methods, so it's an example of the latter case.

4 Likes

Thanks all, especially the (detailed) note about re-entrancy, wasn't something that I had thought about, but helps greatly in my understanding!