Rust FnOnce FnMut Fn the 100th Time
tldr
FnOnceowns the closurefn call_once(self, args: Args) -> Self::OutputFnMutmutable borrows the closurefn call_mut(&mut self, args: Args) -> Self::OutputFnborrows the closurefn call(&self, args: Args) -> Self::OutputFnimplementsFnMut,FnMutimplementsFnOnceFnOncehas less restrictions whereFnhas most restrictions- Rust checks your anonymous function to deduce which call traits to implement
let f = move || {println!("{}", copy_type);};for aCopytype would copy and own it. The original variable is not owned byf- closure could be borrowed, but it may or may not be callable.
FnOncecalling requires ownershipFnMutcallable through a mutable borrowFncallable through a shared borrow
Overview
I was on the subway and thinking about FnOnce, FnMut, Fn for the 100th time and it was not crystal clear for me (again).
So I decided to actually learn it again but this time write it down.
The Need To Pass In a Function
Ever since functions became first class citizens in most programming languages, passing in a function to another function is more of a standard thing now.
Examples like setTimeout takes in a function and that function would be called when the timeout timed out (roughly)
Sometimes you would create a function on the fly to pass to another function. And you probably heard of this thing called closure many times, but what is a closure?
What Is a Closure (In My Mind)
A closure is an abstraction representing your function and the captured environment when the function was created. A good mental model is to envision a struct holding your function logic and the captured environment by either value or reference.
In the function above we created f inside main and called f().
If you look closer you would notice that f refers to x.
When f() was running it somehow knows information about x.
Running this program would print 10.
The reason why f() knows about x is because when f was created the programming language had support to let it “capture” the surrounding environment. Closure is the anonymous function and the environment enclosing it.
Note that the way f captures the environment might be different, it could capture by value, or it could capture by reference.
If we add a few more lines to the example above it suddenly becomes less straight forward.
The second f() call would print 10 if f captured x by value, it would print 11 if f captured x by reference.
In this specific example it is printing 11 because x is captured by reference.
In Go, in order to let f capture x by value you would need to capture it by value by adding a parameter to f
|
|
The above f now takes in x and returns a function. And we created f1 which captures x by value when x was 10.
From FnOnce, FnMut and Fn’s Point of View
Hopefully the closure crash course makes sense.
Now let’s go back to Rust.
FnOnce, FnMut and Fn are traits, and rust closures would implement these traits.
Difference On What Happens When They Are Called
Imagine when Rust creates an anonymous function it creates a struct and that struct implements one or more of these traits. This struct is the closure.
FnOnce’scall_oncewould consume the closure. Meaning it takes inselfFnMut’scall_mutwould mutably borrow the closure. Meaning it takes in&mut selfFn’scallwould borrow the closure. Meaning it takes in&self
It might be a bit more clear to see this by checking the trait required method.
- FnOnce requires
fn call_once(self, args: Args) -> Self::Output - FnMut requires
fn call_mut(&mut self, args: Args) -> Self::Output - Fn requires
fn call(&self, args: Args) -> Self::Output
Difference On What Guarantees They Provide
Now we know FnOnce, FnMut and Fn accesses the closure differently when called.
With that in mind we could understand the following better.
FnOnce Alone Only Guarantees At Most Once Call
So we know that when calling a FnOnce it consumes the closure. This means it could technically drop things in the closure when it’s being called.
This means it is only safe to call FnOnce at most one time.
FnMut Can Be Called Multiple Times But Might Change Things
On the other hand, we know that calling FnMut mutable borrows it’s closure by &mut self, meaning this is a mutable borrow. All restrictions of a mutable borrow in Rust stands.
Fn Can Be Called Multiple Times
And for Fn we know it just borrows the closure so it can be called as much as possible.
Relations Between These Three
Turns out there is a relation between these three.
Fn implements FnMut and FnMut implements FnOnce.
In terms of inheritance this means
Fn inherits FnMut inherits FnOnce
So FnOnce is like the base class, and FnMut is the sub class of FnOnce and Fn is the sub class of FnMut
Kinda like, “Rectangle(Fn) inherits Square(FnMut) inherits Shape(FnOnce)”
Which means, wherever you need an FnOnce you could pass in a FnMut or a Fn.
I found it harder to remember/understand if I try to use the base class sub class mentality.
It feels more natural for me to think in the following way.
FnOnceonly implementscall_once.FnMutimplementscall_onceand on top of that it implementscall_mutFnimplementscall_onceandcall_mutand on top of that it implementscall
So FnOnce implements 1 method, FnMut implements an additional one on top of FnOnce, Fn implements an additional one on top of FnMut.
This means where ever a FnOnce is needed, I can pass in an FnOnce or FnMut or Fn
From The Function’s Point of View
So far I’ve been viewing things from the traits point of view. But there is another view that is also important. The view as a function writer who needs to take in closures.
Before we go there let’s look at this interesting quote from the one and only Hongwei Xi.
The Weaker The Parameter, The Stronger The Function, Vice Versa
Functions has parameters, and we could use types to limit the parameter.
For instance we could have a function adding two integers func Add(a int, b int).
The parameter is more restrictive (strong), so the function doesn’t need to handle much.
On the other hand, if you have func Add(a any, b any), the parameter is loose (it literally says any).
So the function itself needs to handle more cases (more possibilities could be thrown to the function).
One example to think of is in python, the print() function is very strong
So Among FnOnce, FnMut and Fn, Who Is Strong And Who Is Weak?
So who is the stronger one among the three when trying to use them as a function designer?
The answer is Fn is the most restrictive (strong) one and FnOnce is the loose one (weak).
So when you design a function like fn foo(f: impl FnOnce() -> String) you are saying: give me a function that I could call, and I can get by by only calling this function once (if you think about this, it’s kinda the bare minimum you could ask for a function)
Where as if you do fn foo(f: impl Fn()), you are asking for a function that could be called multiple times through a shared reference.
Rust Tries To Find Out What Your Closure Actually Is Doing
One thing that confused me for a long time is in Rust the compiler actually goes through your anonymous function and tells if your function needs to own/mut borrow/borrow variables in the closure.
|
|
Some notes for each case
- (1) Rust saw
fonly needs to borrows, type offisFn. - (2) Rust saw the
push_str()and calls closure with&mut self, type offisFnMut. - (3) Rust saw
drop(s3), calls closure withself, type offisFnOnce. Note that I did not force a move withlet f = move || {}. - (4) Similar to (1), the difference is type of
xisi32and it implementsCopy. Type offisFn. - (5) Similar to (2), Type of
fifFnMut. - (6) Here is the most different one, not in terms of the type of
fbut in terms of what happens tox3. Here type offisFn, I tried to forcefownx3by usingmove. But becausei32implementsCopy, a copy ofx3is actually copied in and owned byf. Meaningfdoes not actually own thex3outside, it owns a copy ofx3inside.
FnOnce, FnMut and Fn Could Be Passed By Reference
It is possible to borrow Fn, FnMut, FnOnce. But then can we still call them?
Let’s have a look at the following which demonstrates what I meant by “possible to borrow”.
Here borrow is of type & impl FnOnce(). Turns out you can not call this closure through a borrow. It kinda makes sense, because there could be multiple borrows, and since calling FnOnce consumes the closure, it can not be called through a borrow.
After understanding the above, the following two would be easier to understand.
FnMut can only be called through a mutable borrow because calling FnMut might mutate the closure so you could only call it via a mutable borrow.
Then the Fn case is the most simple one, you can call Fn through a normal borrow relatively.
To summary, a closure implementing FnOnce only can’t be called through any borrow, a closure implementing FnMut but not Fn can only be called through a mutable borrow, and Fn could be called through any borrow.
| Closure | &closure callable? |
&mut closure callable? |
|---|---|---|
FnOnce only |
No | No |
FnMut but not Fn |
No | Yes, repeatedly |
Fn |
Yes, repeatedly | Yes, repeatedly |
Links
- Crust of Rust: Functions, Closures, and Their Traits
- This video also explains function pointers in rust which I completely forgot
See Also
- Rust State Machine Part 3
- Rust State Machine Part 2
- Rust State Machine Part 1
- Map Flatmap and Options
- Rust Learning: Modules