Let's take a look at Iterator::filter. It doesn't store the predicate
argument, but it needs to mutably called it, so it should take it by mutable reference, like this: predicate: &mut P
, but instead, it takes it by movement of ownership!
EDIT: filter
actually needs to take ownership here, because it's lazy and you can return the filtered iterator to elsewhere without consuming it immidietely, so you actually need to be able to store the predicate. A better example to illustrate my point would be Vec::retain, which is eager, and doesn't need to store the predicate, so it shouldn't need to take it by value.
This happens to work, because &mut FnMut
implements FnMut
with the same generics, but it seems non-idiomatic. So what could be the possible reasons?
Possible reason 1: Performance
I'm no expert on how compiler optimizations work, but let's imagine a simple call like this: .filter(|item| {*item >= 0})
. Since the FnMut
doesn't close over any variables, it is a normal lambda function (not a closure) with size 0, so moving it into the filter
function will be a no-op.
But passing it as a &mut
reference would require to pass a reference to an empty unit struct, which might get optimized to no-op by the compiler if it is smart enough, but it might also not (AFAIK).
Possible reason 2: Ergonomics
- You don't have to bother with the
&mut
borrow, writing.filter(|item| { ... })
is slightly more convenient than writing.filter(&mut |item| { ... })
. - You don't have to annotate the arguments, because the
.filter(&mut |item| { ... })
syntax doesn't actually work. The compiler cannot infer the types, so you have to manually annotate it (e.g..filter(&mut |item: *i32| { ... })
) with the correct amount of*
s.
Possible reason 3: Lifetime annotation
Sometimes you need a lambda that returns a value that is borrowed from one of it's arguments. And unlike in point 2.2, there are no manual annotations that will save you (that I know of).
This GitHub issue goes into more detail on that, including some proposed syntax like <'a>|text: &'a str| -> &'a str { ... }
for example, as well as a "workaround hack" using a cast
function:
fn cast<T>(x: T) -> T
where T: for<'a> Fn(&'a str) -> &'a str
{
x
}
Some examples
Notice that this "hack" is only needed when you want to declare you lambda and use it later. If you use it straight away, you don't need it. But if you want to use it strait await as a reference, points 2.2 and 3 combine and force to use this cast
function: Example that takes Fn
Notice that the last line only works because process_strings
function takes Fn
, and not &Fn
. Example that takes &Fn
.
Back to idiomatic design
The idiomatic way (at least as I understand it) to take arguments is:
- If you only read from it and don't store it, take a shared reference to it (e.g.
&T
). - If you need to write to it and don't store it, take an exclusive reference to it (e.g.
&mut T
). - If you store it, take ownership of it (e.g.
T
).
But that doesn't seem to apply to Fn
/ FnMut
for the reasons listed above. For these 2 traits, the answer seems to be "Always take Fn
/ FnMut
by ownership, even when you don't store them", which seems at odds with the rest of Rust's ownership model design.
But what about other / custom traits?
The real question is: how will this "creep" into other traits? For example, &T
implements Display
if T
implements Display
. That makes sense, but I have seen someone argue that you should take Display
values by ownership instead of references (even when you are not storing them), because "It will work for references too".
Unfortunately, I cannot find the post anymore, but I feel that the design of Fn
and FnMut
actually encourages this kind of thinking.
Possible solutions
Adding a way to annotate lifetimes in lambdas/closures explicitly (e. g. <'a>|text: &'a str| -> &'a str { ... }
) would be great, not only for this reason.
Making argument inference work when the lambda / closure is behind a reference (e. g. .filter(&mut |item| { ... })
) would help with this problem, but not with much else probably.
With these 2 things, we could properly accept &Fn
or &mut FnMut
in functions by references without putting undue restrictions on the caller (at worst an additional &
/ &mut
would be needed).
However, existing function would need to stay the same (the braking change wouldn't be worth it), so then you would have old functions taking Fn
/ FnMut
but ownership, and new functions taking them by references, leading to an even bigger mess than we have now.
The only "real" solution seems to be to add a 4th line to the "idiomatic ways to take arguments" list:
- If it is a
Fn
/FnMut
, take it by ownership anyway.