I have a partly-annotated playground link that is a rough encapsulation of what I am trying to do, but requires a bit of an awkward hack in the form of a sealed-trait Refl<T>
that is implemented only using impl<T> Refl<T> for T
.
I am wondering if there is a more idiomatic Rust solution for this problem, which I will explain below in more detail, and for anyone for whom the playground link isn't inadequately informative.
I have a pair of traits representing infallible (Promote<T>
) and fallible (TryPromote<T>
) conversion from one type T
to whatever Self
the trait is implemented for. Each defines a promotion function, being Promote::promote(orig: &T) -> Self
or TryPromote::try_promote(orig: &T) -> Result<Self, Self::Error>
, where TryPromote
additionally defines an associated type Error
.
I am implementing these traits (whichever is more specific based on the actual fallibility of the conversion) for a bipartite set of types which describe a bijection: on one side, a set of types that were machine-generated, and on the other side, hand-written analogous types to each one I care about (usually eliminating needless indirection and omitting extraneous details). Each set of types exist in a rough type-tree, with many of them encapsulating values of at least one other type within that same set.
When I am implementing TryPromote
out of necessity because the conversion is locally fallible (i.e. an error can be produced outside of a recursive call to another try_promote
method), I define Error
to be a concrete error-type appropriate to the nature of the conversion. The error itself isn't very important, but it is important to note that there are very few distinct errors in the entire hierarchy, and 'novel' sources of errors are relatively uncommon; most impl TryPromote<X> for Y
blocks will typically induce their Error
item from one or more calls to some other instance of the TryPromote::try_promote
method.
Because the set of types is rather large, it is a pain to figure out, let alone remember, whether the conversion from some MachineX
in the machine-generated type-set to its analogue X
in the hand-written type-set, is (a) infallible, (b) inherently fallible due to locally novel errors, or (c) transitively fallible due to calls to other TryPromote conversions. Even when (c) is known, it is further difficult to distinguish (b) and (c) on any of the embedded calls to other try_promote
methods on its component types, and it's basically turtles all the way down because of that.
In order to both avoid having to perform a lot of highly-contextual and difficult-to-decide edits in the case where I realize that one particular TryPromote<X> for Y
should either instead be Promote
, or have a different Error
type than it had originally, I'd like to use trait-associated item qualification to encapsulate the induced error type of any (c) cases, as
impl TryPromote<X1> for Y1 {
type Error = <Y2 as TryPromote<X2>>::Error;
/* ... */
}
where the only possible error encountered when promoting X1 -?-> Y1
is due to a call to Y2::try_promote(_ : &X2)
.
However, this doesn't quite work when there is more than one source of potential error, i.e. when either multiple fields in a record struct/variant or multiple values across disparate branches of a variant-match on an enum-type, are themselves fallible (whether (b) or (c)).
I want some way to write
impl TryPromote<X1> for Y1 {
type Error = <the concrete type referred to by both <Y2 as TryPromote<X2>>::Error and <Y3 as TryPromote<X3>>::Error>;
/* ... */
}
But short of defining a crate-private trait and a type-alias
pub(crate) trait Refl<T> {
type Solution;
}
impl<T> Refl<T> for T {
type Solution = T;
}
pub(crate) type ReflType<T1, T2> = <T1 as Refl<T2>>::Solution;
I can't think of another way to do this at the type level.
Of course, at the impl
level, I could write
impl<E> TryPromote<X1> for Y1
where
Y2: TryPromote<X2, Error = E>,
Y3: TryPromote<X3, Error = E>
{
type Error = E;
/* ... */
}
but this has a less-than-desired end-state when the equality stops holding; instead of the compiler rejecting ReflType< Error1, Error2 >
, my understanding is that this will silently erase the impl itself and only complain at any call-site where Y1: TryPromote<X1>
is required.
If these are the only two approaches that work, that is perfectly fine, but I can't help but feel that there should be a better solution, that is pure-rust like the impl
-level approach but which the compiler rejects outright like the type
-level approach if the equality stops holding.
Is there such a way, either currently or on the roster of upcoming features? or are these two approaches really the only way forward?