Trait Bounds Transitive Inference

Heya, I ran into this issue, where given:

  • TraitParamsT has associated types.
  • TraitParamsConstrained has the same associated types, but with trait bounds.
sample code
trait TypeParamsT {
    type AppError;
    type Input;
    type Output;
}

trait TypeParamsConstrained: TypeParamsT {
    type AppError: std::error::Error + 'static;
    type Input: Input + 'static;
    type Output: Output + 'static;
}

impl<T> TypeParamsConstrained for T
where
    T: TypeParamsT,
    T::AppError: std::error::Error + 'static,
    T::Input: Input + 'static,
    T::Output: Output + 'static,
{
    type AppError = T::AppError;
    type Input = T::Input;
    type Output = T::Output;
}

and when I have:

  • A struct Context<Types> where Types: TypeParamsT (unconstrained)
  • a function that takes a Context<Types>
  • Types: TypeParamsConstrained
sample code
fn run<Types, L>(
    cmd_ctx: &mut CmdCtx<Types>,
    logic: &mut L,
) -> Result<L::ReturnType, <Types as TypeParamsConstrained>::AppError>
where
    Types: TypeParamsConstrained,
{
    // ..
}

Rust still doesn't infer that the associated types have the constrained bounds applied to them.

Snippet (playground):

fn run<Types, L>(
    cmd_ctx: &mut CmdCtx<Types>,
    logic: &mut L,
) -> Result<L::ReturnType, <Types as TypeParamsConstrained>::AppError>
where
    Types: TypeParamsConstrained,
    L: Logic,
    <Types as TypeParamsConstrained>::AppError: From<L::Error> + From<FrameworkError>,
    // Why do these bounds have to be repeated?
    // It appears Rust isn't inferring the associated type constraints from the
    // `TypeParamsConstrained` bound.
    //
    // <Types as TypeParamsT>::Output: Output,
    // <Types as TypeParamsT>::Input: Input,
{
    let CmdCtx { input, output } = cmd_ctx;

    Output::write(output, "Enter some input:\n")?;
//  ------------- ^^^^^^ the trait `Output` is not implemented for `<Types as TypeParamsT>::Output`
//
//  help: consider further restricting the associated type
//
//  <Types as TypeParamsT>::Output: Output

    let line = Input::read(input)?;
//             ----------- ^^^^^ the trait `Input` is not implemented for `<Types as TypeParamsT>::Input`
//
//  help: consider further restricting the associated type
//
//  <Types as TypeParamsT>::Input: Input

    let t = logic.do_work()?;

    Output::write(output, "You entered: ")?;
    Output::write(output, &line)?;

    Ok(t)
}

Is there a way that allows CmdCtx<Types> to use the unconstrained version of the trait, and have the inference be applied transitively?

Repo with the full code: GitHub - azriel91/assoc_type_params: Experiment using single type with associated types to track type params.

#20671

And yes, it's still an issue.

ah, thanks! The link doesn't click for me, but here's it again: where clauses are only elaborated for supertraits, and not other things · Issue #20671 · rust-lang/rust · GitHub

Fixed link: 20671. However, associated bound traits are elaborated.[1] This is something more subtle.

You have bounds on CmdCtx that define the struct fields in terms of TypeParamsT (not constrained). Additionally, there's nothing at the definition level that says the associated types of TypeParamsT and the associated types of TypeParamsConstrained have to be the same. That's the key part. Now, you have a blanket implementation that mostly means that's the case, however,

So in the function body, the type of output is <Types as TypeParamsT>::Output, not <Types as TypeParamsConstrained>::Output, and (as far as the compiler is concerned) those could be different types -- hence the error.

This fixes the error by enforcing that the associated types must be the same between the two traits:

// Force the associated types to be the same between the two traits
trait TypeParamsConstrained: TypeParamsT<
    AppError = <Self as TypeParamsConstrained>::AppError,
    Input = <Self as TypeParamsConstrained>::Input,
    Output = <Self as TypeParamsConstrained>::Output,
>

  1. As are supertrait bounds. ↩︎

4 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.