Ambigous associated type

I am currently writing a compile time SKI evaluator. So far it works well but I noticed something interesting and would like to know why it is not possible.

Some basic information: "Evaluation" is implemented by letting the compiler figure out if there exists an type that satisfies some trait bound. The type it has to find describes the evaluation of the SKI expression so always writing it out is out of question for my use case. The Eval trait:

trait Eval<WorkLeft> {
    type Result;
    fn eval(self) -> Self::Result;
}

Now for the interesting part. The following code works and shows a simple example of how to evaluate SKI expressions:

#[test]
fn test_I() {
    let c4 = USize::<4>;
    let identity = Application(Application(S, K), K);
    let applied = Application(identity, c4);
    let evaluated = applied.eval();
    dbg!(evaluated);
}

Notice how we can call applied.eval() without specifying any types.

Now the next test fails, which tries to do the same as the previous one only using types:

#[test]
fn test_I2() {
    type C4 = USize<4>;
    type identity = Application<Application<S, K>, K>;
    type applied = Application<identity, C4>;
    type evaluated = applied::Result;
}

Here the compiler complains about an ambigous associated type on applied::Result.

My question: the compiler obviously knows to check for the existence of the type parameter WorkLeft and that it is unique (otherwise test_I should give a use fully qualified trait syntax error) on function calls, why does rustc not allow accessing associated types from such a trait?

For the interested the full code (180 lines of code including tests):

Code
use std::marker::PhantomData;

trait Eval<WorkLeft> {
    type Result;
    fn eval(self) -> Self::Result;
}

struct Done;
struct Todo<T>(PhantomData<T>);
struct TodoApplication<L, R>(PhantomData<L>, PhantomData<R>);

trait ConstVal {
    type RuntimeType;
    const VAL: Self::RuntimeType;
}

#[derive(Copy, Clone)]
struct S;
impl Eval<Done> for S {
    type Result = S;
    fn eval(self) -> Self::Result {
        self
    }
}

#[derive(Copy, Clone)]
struct S1<X>(X);
impl<X> Eval<Done> for S1<X> {
    type Result = S1<X>;
    fn eval(self) -> Self::Result {
        self
    }
}

#[derive(Copy, Clone)]
struct S2<X, Y>(X, Y);
impl<X, Y> Eval<Done> for S2<X, Y> {
    type Result = S2<X, Y>;
    fn eval(self) -> Self::Result {
        self
    }
}

#[derive(Copy, Clone)]
struct K;
impl Eval<Done> for K {
    type Result = K;
    fn eval(self) -> Self::Result {
        self
    }
}

#[derive(Copy, Clone)]
struct K1<X>(X);
impl<X> Eval<Done> for K1<X> {
    type Result = K1<X>;
    fn eval(self) -> Self::Result {
        self
    }
}

#[derive(Copy, Clone)]
struct Application<L, R>(L, R);

impl<R, TL, TR, X, Y> Eval<TodoApplication<TL, TR>> for Application<Application<X, Y>, R>
where
    Application<X, Y>: Eval<TL>,
    Application<<Application<X, Y> as Eval<TL>>::Result, R>: Eval<TR>,
{
    type Result = <Application<<Application<X, Y> as Eval<TL>>::Result, R> as Eval<TR>>::Result;
    fn eval(self) -> Self::Result {
        Application(Application(self.0 .0, self.0 .1).eval(), self.1).eval()
    }
}

impl<R, T> Eval<Todo<T>> for Application<S, R>
where
    S1<R>: Eval<T>,
{
    type Result = <S1<R> as Eval<T>>::Result;
    fn eval(self) -> Self::Result {
        S1(self.1).eval()
    }
}
impl<R, T, X> Eval<Todo<T>> for Application<S1<X>, R>
where
    S2<X, R>: Eval<T>,
{
    type Result = <S2<X, R> as Eval<T>>::Result;
    fn eval(self) -> Self::Result {
        S2(self.0 .0, self.1).eval()
    }
}
impl<R, T, X, Y> Eval<Todo<T>> for Application<S2<X, Y>, R>
where
    R: Copy,
    Application<Application<X, R>, Application<Y, R>>: Eval<T>,
{
    type Result = <Application<Application<X, R>, Application<Y, R>> as Eval<T>>::Result;
    fn eval(self) -> Self::Result {
        Application(
            Application(self.0 .0, self.1),
            Application(self.0 .1, self.1),
        )
        .eval()
    }
}

impl<R, T> Eval<Todo<T>> for Application<K, R>
where
    K1<R>: Eval<T>,
{
    type Result = <K1<R> as Eval<T>>::Result;
    fn eval(self) -> Self::Result {
        K1(self.1).eval()
    }
}
impl<R, T, X> Eval<Todo<T>> for Application<K1<X>, R>
where
    X: Eval<T>,
{
    type Result = <X as Eval<T>>::Result;
    fn eval(self) -> Self::Result {
        self.0 .0.eval()
    }
}

// Numbers
#[derive(Copy, Clone, Debug)]
struct USize<const N: usize>;

impl<const N: usize> ConstVal for USize<N> {
    type RuntimeType = usize;
    const VAL: Self::RuntimeType = N;
}

impl<const N: usize> Eval<Done> for USize<N> {
    type Result = Self;
    fn eval(self) -> Self::Result {
        USize::<N>
    }
}

impl<R, T, const N: usize> Eval<Todo<T>> for Application<USize<N>, R>
where
    R: Eval<T>,
{
    type Result = Application<USize<N>, <R as Eval<T>>::Result>;
    fn eval(self) -> Self::Result {
        Application(self.0, self.1.eval())
    }
}

#[cfg(test)]
mod test {
    use super::*;
    #[test]
    fn test_K1() {
        type T = Application<K1<USize<1>>, USize<2>>;
        type R = <T as Eval<Todo<Done>>>::Result;
        const n: usize = R::VAL;
    }
    #[test]
    fn test_K() {
        type T = Application<Application<K, USize<1>>, USize<2>>;
        type R = <T as Eval<TodoApplication<Todo<Done>, Todo<Done>>>>::Result;
        const n: usize = R::VAL;
    }

    #[test]
    fn test_I() {
        let c4 = USize::<4>;
        let identity = Application(Application(S, K), K);
        let applied = Application(identity, c4);
        let evaluated = applied.eval();
        dbg!(evaluated);
    }

    #[test]
    fn test_I2() {
        type C4 = USize<4>;
        type identity = Application<Application<S, K>, K>;
        type applied = Application<identity, C4>;
        type evaluated = applied::Result;
    }
}

Without looking too closely at the whole code, I think you just need to tell rustc that you mean the trait's associated type:

type evaluated = <applied as Eval<something>>::Result;

This also shows why it is otherwise ambiguous: there could be multiple impl Eval<T> for that type for different T.

But the same reasoning could be applied to calling Eval<T>::eval() on applied in the first example, right? But there rustc somehow figures out we mean Eval<T>::eval() for some T and checks that there is only a single T that fits and finally uses it to call eval...

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.