Force unification/equality of two or more trait-associated types

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?

If you only need two, you could...

        impl TryPromote<MachineMiddle> for Middle {
            type Error = <Foo as TryPromote<MachineFoo>>::Error;
            fn try_promote(orig: &MachineMiddle) -> Result<Self, <Bar as TryPromote<MachineBar>>::Error> {

...and you'll get an error if they're not actually equal. Or you could

        impl TryPromote<MachineMiddle> for Middle {
            type Error = <Foo as TryPromote<MachineFoo>>::Error;
            fn try_promote(orig: &MachineMiddle) -> Result<Self, Self::Error>
            where
                Bar: TryPromote<MachineBar, Error = Self::Error>,
                // Add more if you want

which will also error if not tautologically true.

Or if you're OK with the check not being in the API, you could:

        impl TryPromote<MachineMiddle> for Middle {
            type Error = <Foo as TryPromote<MachineFoo>>::Error;
            fn try_promote(orig: &MachineMiddle) -> Result<Self, Self::Error> {
                let ERR: Option<Self::Error> = None;
                let _TYPE_EQ_CK: Option<<Bar as TryPromote<MachineBar>>::Error> = ERR;
                // Add more if you want
1 Like

Those approaches are all interesting to consider, but unfortunately for the first one, I occasionally need more than just 2 associated Error types to equate.

As for the others, they are both worth considering, but things start falling apart due to lack-of-symmetry if the chosen representative <Foo as TryPromote<MachineFoo>>::Error ends up getting nixed due to a TryPromote-to-Promote refactoring. This often would happen more on (c) cases than (b) since, if the error itself doesn't change types, it's usually easier to get the error-type right on the first try when it is produced locally to the impl in question. But when that happens, a lot more needs to get modified than if there is a semi-symmetric approach like either of the two I listed at the end of the OP.

What falls apart, exactly? Needing to edit the associated type and bound list instead of just one or the other per impl?

You could standardize on always implementiong TryPromote, and then provide a blanket Promote implementation:

impl<X,T> Promote<X> for T where T: TryPromote<X, Error=! /* or std::convert::Infallible */> {
    fn promote(x: &T)->Self {
        Self::try_promote(x).unwrap() // infallible
    }
}
1 Like

if it suits your use case, you can use a generic parameter for the error type instead of trying to unify associated types.

example code adapted from the linked playground:

trait TryPromote<T, E>: Sized
{
    fn try_promote(orig: &T) -> Result<Self, E>;
}

// if the error is non locally generated, use a generic type to implement
// and list the fallible sub-conversions in the where clause
// they all generate the same error type
impl<E> TryPromote<MachineMiddle, E> for Middle
where
    Foo: TryPromote<MachineFoo, E>, // Foo is falliable
    Bar: TryPromote<MachineBar, E>, // Bar is falliable
    // no bound for Baz, it is infalliable
{
    fn try_promote(orig: &MachineMiddle) -> Result<Self, E> {
        Ok(match orig {
            MachineMiddle::Foo(foo) => {
                // Identity-cast `?`
                Middle::Foo(Foo::try_promote(foo)?)
            }
            MachineMiddle::Bar(bar) => {
                // Identity-cast `?`
                Middle::Bar(Bar::try_promote(bar)?)
            }
            MachineMiddle::Baz(baz) => {
                // Infallible so no `?`
                Middle::Baz(Baz::promote(baz))
            }
        })
    }
}

// if the error is locally generated, impl with a concrete type
impl TryPromote<MachineFoo, TooBigError> for Foo {
    fn try_promote(orig: &MachineFoo) -> Result<Self, TooBigError> {
        Ok(Self {
            useful_field: orig.useful_field,
            small_number: try_small_u32(orig.small_number)?,
        })   
    }
}

again, it's a different design, I don't know your exact use case, this design might not work for you. (e.g. it may cause conflicts, or you may get inference issue, etc)

Unfortunately this isn't the part of the example that I am attempting to solve. While this is a valid approach, it doesn't actually address the question of 'reference the concrete type shared by two trait-item qualified identifiers', which is the crux of the OP.

Making the Error type an impl-bound type-parameter isn't quite right for my use-case, since it not only makes it impossible to reference the error type associated with a particular conversion, it makes the structurally-inductive process of implementing the TryPromote traits on novel nodes in the type-hierarchy that much more involved, since the impl itself cannot be written, even partially or incorrectly, without having a grasp of the existing impls for all lower-level types that are converted in the try_promote function.

What I mean is that it basically forces a decision of which 'candidate' trait-associated item to lift above the others, rather than referencing them all in the same breath. If we use <X as Trait>::Type to refer to the common value of <X as Trait>::Type and <Y as Trait>::Type, we lose sight of the fact that <Y as Trait>::Type is also included, and so if we were to then retool it so that impl Trait for X no longer exists (in this case, turning a TryPromote impl into a Promote impl), the fall-through onto <Y as Trait>::Type is slightly obscured by the pattern in question. It's not a huge impact overall since they appear either in the try_promote where clause or function body for the two latter suggestions, but I'd be more inclined to go with the ReflType or impl<E> approach over those two for reasons of symmetry (i.e. our only edits are line-local, rather than needing to look further down to figure out the next candidate to use instead of the missing <X as Trait>::Type we originally chose)

Right.

There's no direct way to do this:

type Error = <<Y2 as TryPromote<X2>>::Error and <Y3 as TryPromote<X3>>::Error>;

I can't think of a better way to do it indirectly all "inline" than something akin to your trait approach.

        pub trait AllEq {
            type Type;
        }

        impl<T> AllEq for (T, T) {
            type Type = T;
        }
        impl<T> AllEq for (T, T, T) {
            type Type = T;
        }

        // ...

            type Error = <(
                ConvError<MachineFoo, Foo>,
                ConvError<MachineBar, Bar>,
            ) as AllEq>::Type;
2 Likes