Unconstrained type parameter when using generic trait

Hello!

I have something like the following snippet.

trait Parser<In, Out> {
    fn try_parse(input: In) -> Out;
}

struct MapToInt<P> {
    parser: P 
}

impl<P, In, Out> Parser<In, usize> for MapToInt<P>
where
    P: Parser<In, Out>
{
    fn try_parse(input: In) -> usize {
        todo!()
    }
}

Understandably, it does not compile with:

the type parameter Out is not constrained

I understand why: the type does not appear in the implemented trait, or the implementing struct, or an associated type.

Because of API design reasons*, I cannot convert this trait to use associated types instead. Is using PhantomData really the only way out of this?


[*]: My reason here is that I want to implement the Parser trait for any function as well. Assuming I switch Parser to use associated types instead, now this stops working because of unconstrained types

trait Parser2 {
    type In;
    type Out;
    fn try_parse(input: Self::In) -> Self::Out;
}

impl<F, In, Out> Parser2 for F
where
    F: Fn(In) -> Out
{
    type In = In;
    type Out = Out;
    
    fn try_parse(input: Self::In) -> Self::Out {
        todo!()
    }
}

This can be solved with one more level of indirection, by forcing functions to expose their return type as an associated type through a custom trait.

The essence is defining FnAssoc, implementing it for functions and MapToInt, then implementing all parsers in terms of this trait using a blanket impl:

trait FnAssoc<In> {
    type Out;

    fn do_call(self, input: In) -> Self::Out;
}

impl<In, Out, F> FnAssoc<In> for F
where
    F: FnOnce(In) -> Out
{
    type Out = Out;

    fn do_call(self, input: In) -> Out {
        self(input)
    }
}
1 Like

Huh. Clever! That does look promising :thinking:

But how come Out is not unconstrained in this impl? I thought trait bounds were not relevant for constraining a type parameter (except in the case that the type parameter is bound as an input to an associated type)

impl<In, Out, F> FnAssoc<In> for F
where
    F: FnOnce(In) -> Out
{
    type Out = Out;
    // ...
}

F: FnOnce(In) -> Out is syntactic sugar for the unstable F: FnOnce<(In,), Output=Out>, hence Out is actually an input to an associated type.

1 Like

Yeah. The trick is merely syntactic. The Fn*(I) -> O syntax doesn't let you get the return type, but if you arrange for it syntactically using a regular user-defined trait, you can still do this because it's still an associated type.

Huh. TIL! Is this mentioned somewhere in the stdlib docs i missed? Or is it more of an obscure trick? It does open up a few more doors in my implementation, so I'll play around with it. Thank you!

Hmm and just to clarify, if I didn't want to go the route of associated types (i.e. i wanted the Parser trait to remain fully generic), implementing Parser<..> for MapToInt (as in the original code snippet) would require the use of PhantomData ? there is no way around that?

I don't think it's particularly obscure. It's logical that a function's return type is an associated type. After all, a function's arguments determine its return value, so it's not surprising that the types of the arguments determine the type of the return value.

:thinking: not sure if i completely follow that line of reasoning, but I just realized that the documentation of the Fn trait in the standard library does hint at the return type being an associated type (i.e. Self::Output), so its not as obscure as i thought.

I was able simplify your solution and remove the extra trait. I think the key is making the input type generic and the output type an associated type.

I think this modified version will work for me.

I appreciate the time you spent looking into the problem with me!

For posterity I am marking the answer to this question as

"Keep the input type generic, and make the output type an associated type".

I didn't get to this on my own. User @H2CO3's answer above got me 90% of the way there.

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.