★ Revisiting Rust's Function Pointers
— 6 minutes read
I once needed a function that takes an optional function pointer. This is quite easy to express in Rust:
fn maybe_closure<T: FnMut(String) -> ()>(closure: Option<T>) -> () {
...
}
maybe_closure(Some(...));
But I ran into a problem calling maybe_closure
with None
maybe_closure(None);
because the compiler could not infer the concrete type of None
in this context
11 | maybe_closure(None);
| ^^^^^^^^^^^^^^^^^^ cannot infer type for `T`
|
= note: type annotations or generic parameter binding required
even when I specified the type using the turbo fish syntax
|
11 | maybe_closure(None::<FnMut(String) -> ()>);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `std::marker::Sized` is not implemented for `std::ops::FnMut(std::string::String)`
|
= note: `std::ops::FnMut(std::string::String)` does not have a constant size known at compile-time
= note: required by `std::prelude::v1::None`
So I asked the friendly Rust users and xfix provided the correct answer
maybe_closure(None::<fn(_)>);
As history repeats itself, I was confronted with a similar question recently and to my surprise googled my exact same question from a year ago. Now it’s time to understand the details.
Option
is a Generic Type
First, why does the compiler need to infer type for T
for None
?
Option
is an enum that holds a generic data type in one of its variants, i.e., Some(T)
. Here’s the definition of Option
:
pub enum Option<T> {
None,
Some(T),
}
So Option<T>
is a type constructor for concrete types with type parameter T
. This means that Option<A>
and Option<B>
with A != B
are two different types. For example Some(10)
and Some("10")
are of different types as well as None::<i32>
and None::<&str>
.
Since None
does not explicitly state of which concrete type it is, the compiler cannot infer the concrete type in a situation as above when we just pass None
to the maybe_closure
function.
Fn
, FnMut
, and FnOnce
are Traits
So why didn’t maybe_closure(None::<FnMut(String) -> ()>);
solve the problem?
Since Fn
, FnMut
, and FnOnce
are traits they do not have a constant size. Only concrete types have a constant size and thus, only concrete types that implement a trait may be passed to a function. We could use Box<T>
which is Rust’s smart pointer for data on the heap to get a constant size parameter for our function, but that would make using the our maybe_closure
even more cumbersome.
fn
Primitive Type The solution xfix pointed out uses the primitive type fn
which is a function pointer. Since this is a primitive type, it has a concrete size. But why does fn(_)
type check for maybe_closure
? The explanation can be found in the Rust documentation for fn
:
In addition, function pointers of any signature, ABI, or safety are Copy, and all safe function pointers implement Fn, FnMut, and FnOnce. This works because these traits are specially known to the compiler.
So fn
implements FnMut
and is of constant size.
Summary
Passing functions and closures as parameters to functions requires constant sizes. If the parameter should be an optional None
, the compiler needs explicit type information to type check the function call.
This simple Rust program illustrates this in detail. It’s also available as gist on Github and on Playground.
// Takes a function pointer from i32 to i32
fn takes_a_fp(f: fn(i32) -> i32, value: i32) -> i32 {
f(value)
}
// Takes a closure with the trait bound Fn(i32) -> i32
fn takes_a_closure<T: Fn(i32) -> i32>(f: T, value: i32) -> i32 {
f(value)
}
// Takes a closure with the trait bound Fn(i32) -> i32 using 1.26 impl syntax
fn takes_a_closure_impl(f: impl Fn(i32) -> i32, value: i32) -> i32 {
f(value)
}
// Takes an optional function pointer from i32 to i32
fn takes_an_opt_fp(f: Option<fn(i32) -> i32>, value: i32) -> Option<i32> {
if let Some(f) = f {
Some(f(value))
} else {
None
}
}
// Takes an optional closure with the trait bound Fn(i32) -> i32
fn takes_an_opt_closure<T: Fn(i32) -> i32>(f: Option<T>, value: i32) -> Option<i32> {
if let Some(f) = f {
Some(f(value))
} else {
None
}
}
// Takes an optional closure with the trait bound Fn(i32) -> i32 using 1.26 impl syntax
fn takes_an_opt_closure_impl(f: Option<impl Fn(i32) -> i32>, value: i32) -> Option<i32> {
if let Some(f) = f {
Some(f(value))
} else {
None
}
}
// Function from i32 -> i32
fn id(x: i32) -> i32 {
x
}
fn main() {
// Closure from i32 -> i32
let closure = |x: i32| x;
println!("takes_a_fp(closure, 42): {}", takes_a_fp(closure, 42));
println!("takes_a_fp(fn, 42): {}", takes_a_fp(id, 42));
println!();
println!("takes_a_closure(closure, 42): {}", takes_a_closure(closure, 42));
println!("takes_a_closure(fn, 42): {}", takes_a_closure(id, 42));
println!();
println!("takes_a_closure_impl(closure, 42): {}", takes_a_closure_impl(closure, 42));
println!("takes_a_closure_impl(fn, 42): {}", takes_a_closure_impl(id, 42));
println!();
println!("takes_an_opt_fp(closure, 42): {:?}", takes_an_opt_fp(Some(closure), 42));
println!("takes_an_opt_fp(fn, 42): {:?}", takes_an_opt_fp(Some(id), 42));
// The type of `f` is unambiguous, therefore Rust can infer the correct type of Option<f> for None
println!("takes_an_opt_fp(None, 42): {:?}", takes_an_opt_fp(None, 42));
println!();
println!("takes_an_opt_closure(closure, 42): {:?}", takes_an_opt_closure(Some(closure), 42));
println!("takes_an_opt_closure(fn, 42): {:?}", takes_an_opt_closure(Some(id), 42));
println!("takes_an_opt_closure(None, 42): {:?}", takes_an_opt_closure(None::<fn(_) -> i32>, 42));
println!();
println!("takes_an_opt_closure_impl(closure, 42): {:?}", takes_an_opt_closure_impl(Some(closure), 42));
println!("takes_an_opt_closure_impl(fn, 42): {:?}", takes_an_opt_closure_impl(Some(id), 42));
// The type of `f` is unambiguous, therefore Rust can infer the correct type of Option<f> for None
println!("takes_an_opt_closure_impl(None, 42): {:?}", takes_an_opt_closure_impl(None::<fn(_) -> i32>, 42));
}