(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