struct Foo {}
impl Foo {
fn to_bar(&self, maybe: bool) -> Option<Box<dyn Bar>> {
maybe.then(|| Box::new(0usize))
}
}
impl Bar for usize {}
trait Bar {}
This is the kind of thing that I'd usually expect to work, but it fails:
rror[E0308]: mismatched types
--> src/lib.rs:5:9
|
4 | fn to_bar(&self, maybe: bool) -> Option<Box<dyn Bar>> {
| -------------------- expected `Option<Box<(dyn Bar + 'static)>>` because of return type
5 | maybe.then(|| Box::new(0usize))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `Option<Box<dyn Bar>>`, found `Option<Box<usize>>`
|
= note: expected enum `Option<Box<(dyn Bar + 'static)>>`
found enum `Option<Box<usize>>`
= help: `usize` implements `Bar` so you could box the found value and coerce it to the trait object `Box<dyn Bar>`, you will have to change the expected type as well
For more information about this error, try `rustc --explain E0308`.
By contrast, this works:
struct Foo {}
impl Foo {
fn to_bar(&self, maybe: bool) -> Option<Box<dyn Bar>> {
match maybe {
true => Some(Box::new(0usize)),
false => None,
}
}
}
impl Bar for usize {}
trait Bar {}
So I guess it has something to do with inferencing not being able to see "through" the combinator?
There are several posts on the users forum about this. For example this one. It has nothing to do with "combinators"; but instead as stated above, Box::new not being a coercion site. The aforementioned link discusses reasons why this may be difficult to change.
In general, there has to be some type annotation that provokes a coercion at some point before the Box value is wrapped in Option β once itβs in Option it can no longer be coerced to a trait object (Option doesn't implement CoerceUnsized).
Could Option be CoerceUnsized if it contains a CoerceUnsized? I donβt immediately see any problems with that (you can always summon it explicitly with map).
As I understand the design of Rust's type inference and coercion, the "correct" language fix would be to allow coercing from Option<Box<impl Trait>> to Option<Box<dyn Trait>>, or in implementation terms:
Based on the current documentation description of the mechanism behind CoerceUnsized:
For custom types, the coercion here works by coercing Foo<T> to Foo<U> provided an impl of CoerceUnsized<Foo<U>> for Foo<T> exists. Such an impl can only be written if Foo<T> has only a single non-phantomdata field involving T. If the type of that field is Bar<T>, an implementation of CoerceUnsized<Bar<U>> for Bar<T> must exist. The coercion will work by coercing the Bar<T> field into Bar<U> and filling in the rest of the fields from Foo<T> to create a Foo<U>. This will effectively drill down to a pointer field and coerce that.
The program is that, while Option<T>does only have a singular "field" involving T[^0], it is only conditionally inhabited. So we'd effectively need to insert a logical function call into the desugar, since the current MIR cast is a simple rvalue usable within one MIR basic block.
This is doable, of course, like how the other operators have intrinsic behavior for primitive types and a function call for customizable behavior. But Rust by design doesn't have extendable coercions, so enabling that (even if perma-unstable) might be a difficult sell.
I don't think you'd need a function call β you'd just need to make coercion work on enums as well as structs (i.e. this would be a language-level special case rather than custom library code).
I'm not sure that this sort of thing is sound, though β coercing impl Trait to dyn Trait doesn't change the layout of the thing that implements Trait itself (just the layout of the reference/pointer to it), whereas coercing Option<impl Trait> to Option<dyn Trait> can change the layout of the Option (dyn Trait doesn't have a niche, impl Trait might). That means that Option here is acting more like a reference than it is like a struct, which means that the existing safety guarantees wouldn't apply. I don't know whether or not there might be alternative safety guarantees that work instead.
dyn Trait isn't a pointer. Box<dyn Trait> and Rc<dyn Trait> and &dyn Trait are represented internally as pointers. But dyn Trait itself is just some arbitrary type that implements Trait and you don't know what it is. For example:
let x = Box::new(4u64);
let y: Box<dyn Debug> = x;
y is a Box<dyn Debug> and is two pointers (one to a 4u64 on the heap, one to the vtable for u64). *y, which is 4u64, is a dyn Debug (which happens to be a u64). u64 doesn't have a niche, so dyn Debug doesn't either.
This is the same relationship as that between &str (which is a pointer + length) and str (which is an arbitrary number of bytes that happen to be valid UTF-8) β the &str points to the str, and the Box<dyn Trait> points to the dyn Trait.
That's not the goal. The goal is coercing Option<Box<impl Trait>> to Option<Box<dyn Trait>>.
Box<impl Trait>: CoerceUnsized<Box<dyn Trait>>, and impl Trait: Unsize<dyn Trait>.
While you could have the compiler directly synthesize the MIR, yes, it would need to emit a branch and than one basic block. Complexity wise in the implementation this is much worse than a function call.
Ah, I see. I'm still not convinced that sort of thing is always sound, though (impl Trait and dyn Trait can have different alignment, so one Box has more niches than the other β this doesn't matter for Option but might matter for an enum with more cases, if Rust started using alignment niches).
It might be a more complex implementaiton, but it's likely easier to prove sound (e.g. StructuralPartialEq is more complex than PartialEq, but the former is more useful for safety proofs).