Why Rust allows Boxing impl FnMut() and reports `mismatched types` in Option<impl FnMut>?

I have the following struct with implementation of the new() method:

pub struct Subscriber<NextFnType> {
    next_fn: Box<dyn FnMut(NextFnType)>,
    complete_fn: Option<Box<dyn FnMut()>>,
}

impl<NextFnType> Subscriber<NextFnType> {   
    pub fn new(next_fnc: impl FnMut(NextFnType) + 'static,
               complete_fnc: Option<impl FnMut() + 'static>) -> Self {

        let mut s = Subscriber {
            next_fn: Box::new(next_fnc), // <-- Ok
            complete_fn: None,
        };

        if let Some(cfn) = complete_fnc {
            s.complete_fn = complete_fnc.take().map(|f| Box::new(f)); // <-- Not Ok
        }
        s
    }
}

Rust allows boxing first parameter impl FnMut(NextFnType) + 'static, but reports mismatched error when I try to box second parameter which is optional Option<impl FnMut() + 'static>.

Compiler reports:

expected enum Option<Box<(dyn FnMut() + 'static)>>
found enum Option<Box<impl FnMut() + 'static>>

I know I can use type Option<Box<(dyn FnMut() + 'static)>> as a second type parameter but I don't want to change function signature.

Is there a way to Box it like this, and why does Rust allows boxing impl types when type is standalone and forbids when type is wrapped in Option?

use .map(|f| Box::new(f) as Box<dyn FnMut()>.
On mobile can't explain in detail. Simply speaking, Option<Box<T>> does not support unsize coercing, but Box<T> does. So you have to coerce early yourself.

2 Likes

If you don't use take and just rewrap with a new option it works

if let Some(cfn) = complete_fnc {
    s.complete_fn = Some(Box::new(cfn));
}

Playground

As @zirconium-n says, unsize coercions only happen for specific types. Option isn't one of them, so even though Box does support unsize coercions, the fact that it's inside an Option prevents the coercion.

The difference between my version above and your original is the inner type of the Option. In your version the Option's inner type is taken from the return type of the map closure, which is Box<impl FnMut()>. In my version the inner type is known to be Box<dyn FnMut()> because the expression is being directly assigned a field with that type.

If you wrap my working version with a closure, it once again fails to compile since the inner type of the Option is now decided inside the closure. Type inference selects the "obvious" return type for the closure Option<Box<impl FnMut()>> and after that point the unsizing coercion can't be performed.

if let Some(cfn) = complete_fnc {
    s.complete_fn = (|| Some(Box::new(cfn)))();
    //              ^^^^^^^^^^^^^^^^^^^^^^^^^^ expected trait object `dyn FnMut`, found type parameter `impl FnMut() + 'static`
}
2 Likes

Thanks guys!

Somehow I missed to do what you did, get the value out of Option and Box it, or maybe I did and did something wrong in the process and gave up on it.
But it is better this way since I had no idea about unsize coercions, and coercing early.
Your second closure example really clarified it for me.
If I understood correctly, impl types are Sized and dyn types are Unsized, hence "unsized coersion" which is supported only by some types, Box being one of them but not the Option, and in this case closures first return Option.

dyn Trait types are dynamically sized ("unsized") because you can coerce any Sized implementer of the trait to dyn Trait, so it doesn't have a single, statically known size.

Similarly, slices ([T]) and str are dynamically sized based on their length.

Rust doesn't support unsized types on the stack yet, so you can't put them directly in a variable, pass them as function parameters, or return them from functions directly. Instead they have to be behind some sort of pointer like a reference, or in a Box. Hence why you've probably seen &str and &[T] all over the place. Box is similar, but instead of being a temporary borrow, it owns what's behind the pointer (and it puts the data on the heap).

Option doesn't allocate and store things on the heap (or otherwise behind a pointer) like Box does, so it can't support the unsizing coercion.


I'm not exactly sure what you meant by "impl types". But unsized types can still have implementations and implement traits, too.

Most places where you declare a generic type parameter have an implicit Sized bound, perhaps that has something to do with what you meant. You can disable the implicit bound like so:

fn bar<T: ?Sized>(_: &T) {}
4 Likes

That was my amateurish way of saying that if function has impl SomeTrait parameter that takes ownership, type passed to it must be known at compile time (Sized) or "unsized" but de referenced from the heap trough type like Box that supports unsize coercion.

For example I realize we can do this:

trait SomeTrait: std::fmt::Debug {}

impl SomeTrait for [u8] {}

fn foo(v: &(impl SomeTrait + ?Sized)) {
     println!("{:?}", v);
}

fn main() {
    let a: [u8; 5] = [1, 2, 3, 4, 5];
    let slice = &a[0..3];

    foo(slice);
}

But we are using references here which always have known size. If I'm not wrong no unsize coercion happens in this case.

Example where ownership is taken:

trait SomeTrait: std::fmt::Debug {}

#[derive(Debug)]
struct SomeStruct {}

impl SomeTrait for SomeStruct {}

fn bar(v: impl SomeTrait) {
     println!("{:?}", v);
}

fn main() {
    let on_stack = SomeStruct {};
    bar(on_stack);

    let on_heap = Box::new(SomeStruct {});
    bar(*on_heap); // <--  I'm guessing unsize coercion happens here?
}

Ah those. impl Trait has multiple meanings unfortunately. In argument position (APIT), it's practically the same as an anonymous generic type parameter with a bound. In return position (RPIT) and soon other positions like type aliases (TAIT), it's an opaque alias for some other concrete type chosen by the writer of the function.

In all stable positions so far, it has the same implicit Sized bound as a type parameter, which is why you need + ?Sized in the first example.


No unsize coercion could happen in your second example because you can't pass something that's not Sized and you didn't indicate ?Sized.

That said, I don't think that unsize coercion ever implicitly happens for impl Trait + ?Sized in the current language. However, I could imagine it being possible in the future with TAIT or such.


Now to go off on a tangent, because why not.

Box has some magic behavior in that you can move something outside of the box via dereferencing. So here:

    let on_heap = Box::new(SomeStruct {});
    bar(*on_heap);

You put something on the heap then immediately move it back to the stack, which there's no need for. Now, I'm guessing you did this because without the dereference, you get an error, because Box<SomeStruct> doesn't implement SomeTrait.

You can implement Box<SomeStruct> for SomeTrait if you wanted. But perhaps you were really testing that it didn't turn into a Box<dyn SomeTrait>. If so, however, that test wouldn't have worked either, because Box<dyn Trait> doesn't automatically implement Trait.

When you take Box<dyn Trait> as an argument, you're taking a concrete type; you can use things like a Box<dyn Iterator<Item = ()>> because of a combination of auto-dereferencing for method calls (to get through the Box), and the fact that the compiler understands that dyn Iterator implements Iterator -- just like I can take a String as an argument and use its Display implementation without reminding the compiler that String implements Display.

Now, you can implement Trait for Box<dyn Trait> too, if you want. But the coercion from Box<SomeStruct> to Box<dyn SomeTrait> still doesn't happen automatically. You can force it with a cast, though.

However it's not really about impl Trait specifically at all, it's more that unsized coercion isn't automatically applied when passing to a function.

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.