Rust: Passing a closure to a trait object
I have an object which is a
&SomeTrait
, and I want to create a closure and pass it into a method on that trait. How do I make the types work?
In this post I'm going to go through my process of eventually figuring out the answer to this question, starting with a much simpler problem. If you just want the answer, then scroll right to the bottom of this post. Otherwise, read on!
Passing a function into a function
Lets forget about closures and traits for now, and start with a function that takes another function as a parameter:
fn call_function(function: fn(u8) -> bool) -> bool {
function(8)
}
As you can see, all this function does is call its argument with a fixed value, and return the result.
Here's how we might use it:
fn main() {
println!("{}", call_function(my_local_function));
}
fn my_local_function(arg: u8) -> bool{
let max = 5;
arg < max
}
Given a pre-defined a function that does some arbitrary calculation, passing it into our first function above is trivial. You can see the complete example in the Rust playground here, and even run it in your browser!
Closures are not functions
Passing a function into a function is great, but what if we need to pass in a closure instead, one which captures (closes over) its environment?
fn main() {
let max = 5;
let closure = move |arg: u8| { arg < max };
println!("{}", call_function(closure));
}
This is mostly the same as before, except we use a closure instead
of the pre-defined function. Notice that the closure takes
ownership of max
from its environment, using
move
.
Unfortunately,
this doesn't compile. In Rust, closures and functions aren't interchangeable. In
fact, even 2 closures with the same type signature
aren't interchangeable! So we can't use fn(u8) -> bool
as the type
of the parameter to call_function
, because our
closure isn't compatible with it.
Instead, we can make our original function generic, and put a
constraint on the type parameter which says that it has to
implement the
Fn
trait (a trait is like an interface):
fn call_function<F>(function: F) -> bool
where F: Fn(u8) -> bool {
function(8)
}
With that type signature, and our previous attempt at
main
(where we passed in the closure),
it works! If the above code doesn't make sense, read more about
generics
and
static dispatch
in Rust.
How about a method from a trait?
For my particular use case, it wasn't enough to be able to pass a closure into a function. I want to be able to pass a closure into a method, where the object that I'm passing it to just has a trait type. I.e., I don't know what the concrete type is.
Lets take the generic function signature we just created, and make
it part of a trait (with an additional
&self
parameter):
trait FunctionCaller {
fn call_function<F>(&self, function: F) -> bool
where F: Fn(u8) -> bool;
}
Now lets consider a function that takes a
FunctionCaller
(a trait object of unknown concrete
type), and calls its method, passing it a closure:
fn invoke(function_caller: &FunctionCaller) -> bool {
let max = 5;
let closure = move |arg: u8| { arg < max };
function_caller.call_function(closure)
}
The &
means that it's a reference, one of
Rust's several pointer types, which is necessary for
dynamic dispatch
on a trait object in Rust.
Now we just have to create a type that implements our trait,
instantiate it, and pass invoke
a reference to it!
struct MyFunctionCaller {
data: u8,
}
impl FunctionCaller for MyFunctionCaller {
fn call_function<F>(&self, function: F) -> bool where F: Fn(u8) -> bool {
function(self.data)
}
}
fn main() {
let my_function_caller = &MyFunctionCaller{data: 8};
println!("{}", invoke(my_function_caller));
}
... or so I thought. If you try to run
the complete example, it won't let us pass my_function_caller
to
invoke
as a FunctionCaller
:(
error: cannot convert to a trait object because trait
FunctionCaller is not object-safe [E0038]
I don't fully understand object safety yet (this is on my reading list), but I think the basic problem here is that you can't put a generic method in a trait. Which makes some intuitive sense, I didn't really expect it to work as I was trying it.
Boxed closures to the rescue!
While the above compilation error makes sense, it didn't lead me to a solution. If you did try to run that last example though, you might have noticed a second compilation error:
error: the trait FunctionCaller is not implemented for the type
FunctionCaller [E0277]
Uh, ok, that's not immediately helpful... But it did lead me to this on Stack Overflow, which finally gave me what I needed: boxed closures.
Now that I know what the solution is, it seems obvious. In this post, we've already seen two different ways to specify a parameter's type as a trait:
-
Static dispatch with generics:
fn foo<T>(t: T) where T: SomeTrait
-
Dynamic dispatch with pointers:
fn foo(t: &SomeTrait)
What we're trying to do now is have a parameter where
arguments need to implement the Fn
trait. If we
can't use the first approach (generics), then the solution is
surely to use a pointer!
trait FunctionCaller {
fn call_function(&self, function: &Fn(u8) -> bool) -> bool;
}
Then to use it we just need to pass a reference to our closure:
let closure = &move |arg: u8| { arg < max };
And it finally works! In Rust, a pointer to a closure is known as a 'boxed
closure'. The name is a little bit misleading, as Rust's
heap-allocated pointer type is called
Box
, but either pointer type (Box
or reference) will do
the trick. In fact, I originally used Box
in this
blog post, as I thought that was required for boxed closures, so
thanks to
/u/masklinn on
reddit for correcting me!
For completeness, here's the final code I came up with:
trait FunctionCaller {
fn call_function(&self, function: &Fn(u8) -> bool) -> bool;
}
struct MyFunctionCaller {
data: u8,
}
impl FunctionCaller for MyFunctionCaller {
fn call_function(&self, function: &Fn(u8) -> bool) -> bool {
function(self.data)
}
}
fn main() {
let my_function_caller = &MyFunctionCaller{data: 8};
println!("{}", invoke(my_function_caller));
}
fn invoke(function_caller: &FunctionCaller) -> bool {
let max = 5;
let closure = &move |arg: u8| { arg < max };
function_caller.call_function(closure)
}
Phew!
That was tricky stuff! After spending a a few years working mostly in dynamically typed languages (Ruby, Javascript, Python), it's taken me a while to get used to solving problems like these, but I have to say I'm really enjoying it. I'd like to do another post soon on my general experiences with Rust so far, but the short version is that it's awesome. Cancel all your current projects and rewrite everything in Rust.
Thanks for reading!