(This post is related to my previous one but stands on its own, it probably stems from a misunderstanding about how specialization works. Also, sorry in advance for mistakes, this is not my area.)
To solve a seemingly unrelated problem, I needed a generic transformation trait from T to U with default identity implementation (T->T) . Since this requires specialization, I tried the following approach (which compiles in isolation):
pub trait ConvertOutput {
type Output;
}
impl<T> ConvertOutput for T {
default type Output = Self;
}
pub trait Convert: ConvertOutput {
fn into(input: Self) -> Self::Output;
}
impl<T: ConvertOutput<Output = T>> Convert for T {
fn into(input: Self) -> Self::Output {
input
}
}
The problem is that when I try to use this with a concrete type it fails:
fn foo() {
let x = 42_i32;
let _ = Convert::into(x);
}
As I understand it, the compiler sees that i32 implements ConvertOutput via the blanket impl, where Output defaults to i32, however, due to the default keyword, Output is treated as an "opaque type", i.e., it ignores the actual type of Output, and for this reason, i32 does not implements Convert, since the blanket implementation of Convert requires Output to be i32.
This seems related to: Hazard: interactions with type checking section in the specialization RFC where they show a "bad" example, and their solution was to treat any default associated type as opaque. Here is their example for reference:
trait Example {
type Output;
fn generate(self) -> Self::Output;
}
impl<T> Example for T {
default type Output = Box<T>;
fn generate(self) -> Box<T> { Box::new(self) }
}
impl Example for bool {
type Output = bool;
fn generate(self) -> bool { self }
}
fn trouble<T>(t: T) -> Box<T> {
Example::generate(t)
}
fn weaponize() -> bool {
let b: Box<bool> = trouble(true);
*b
}
However, in my case, the default associated type Output is intentionally separated from the method into to avoid these issues, so I don't see why it must be made opaque.
In fact, even in their case, separating the associated type from the method by splitting the trait Example into two separate traits should, I think, prevent all their problem. Consider this modification for their "bad" example implementation:
trait ExampleOutput {
type Output;
}
trait Example: ExampleOutput {
fn generate(self) -> Self::Output;
}
impl<T> ExampleOutput for T {
default type Output = Box<T>;
}
impl ExampleOutput for bool {
type Output = bool;
}
impl Example for bool {
fn generate(self) -> Self::Output { self }
}
For the blanket implementation of Example we have two options:
// Option1 ("full" blanket implementation)
impl<T> Example for T {
fn generate(self) -> T::Output { Box::new(self) }
}
// Option2 ("restricted" blanket implementation)
impl<T: ExampleOutput<Output=T>> Example for T {
fn generate(self) -> T::Output { Box::new(self) }
}
With Option1, the function fn trouble will not compile, and would need to be modified in the following way:
// This will not compile since for a *general* T the
// associated type is a *general* Output and not Box<T>.
fn trouble<T>(t: T) -> Box<T> {
Example::generate(t)
}
// The fix:
fn trouble<T>(t: T) -> T::Output {
Example::generate(t)
}
But then fn weaponize will fail to compile since in the line let b: Box<bool> = trouble(true); the type returned from fn trouble will be <bool as ExampleOutput>::Output which is bool and not Box<bool>.
With Option2, fn trouble will not compile and would need the following fix which restricts the type
fn trouble<T: ExampleOutput<Output=Box<T>>>(t: T) -> Box<T> {
Example::generate(t)
}
which will again make fn weaponize fail to compile in the line let b: Box<bool> = trouble(true);, where now it will fail since bool will not be accepted by fn trouble.
Did I miss anything here? Is there another reason to keep a defaulted associated type opaque even if there are no methods (/ other associated types?) in the same trait?
Moreover, I don't even see how their example compiles even when the defaulted associated type and the method are in the same trait. In that case, for a general type T the function fn generate returns a general Output and not Box<Output> and thus
// this will fail:
fn trouble<T>(t: T) -> Box<T> {
Example::generate(t)
}
// this will not fail, but does not make problems as covered above.
fn trouble<T>(t: T) -> <T as Example>::Output {
Example::generate(t)
}
Thank you in advance