This is an issue that came up while I was writing a pretty terrible parser, so I'll give a simplified reproduction case here.
Basically, consider a recursive enum
defined like this:
enum Expr<V, T> {
Var(V),
Int(T),
Add(Box<Self>, Box<Self>),
}
One of the things that I think is relatively obvious to want to do is to treat a type of this form like a bifunctor, so I wrote a method (basically trying to replicate lmap
/ first
from the functional world) like this:
impl Expr<V, T> {
fn map_vars<F, U>(self, op: F) -> Expr<U, T>
where
F: Fn(V) -> U,
{
match self {
Expr::Int(int) => Expr::Int(int),
Expr::Var(var) => Expr::Var(op(var)),
Expr::Add(lhs, rhs) => Expr::Add(
Box::new(lhs.map_vars(&op),
Box::new(rhs.map_vars(&op))
),
}
}
}
This seemed to work –– cargo check
said it was fine –– but when I actually tried to build the binary, I hit the recursion limit. The error elided 128 redundant requirements, and then said:
required for `&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&...` to implement Fn<(&str,)>
The actual type name (which had to be written to a file) was a closure preceded by 129 &
s. This was a horrific diagnostic to receive, so I immediately gave up and went to change my implementation .
As it turned out, the immediate solution for me was to make
op
a function pointer (fn(V) -> U
) and to remove the borrows in theExpr::Add
branch (since function pointers areCopy
). A slightly better solution (I think) would be to makeop
a&F
, since this prevents the recursive borrowing in the original case.
As far as I can tell, the issue here was that
- the
Fn
trait has a blanket impl such that ifop
implementsFn
, then&op
also implementsFn
, and; - on each recursive invocation of
map_vars
, a new&
is prepended to the argument, so; - the trait-solver has to endlessly check that
op
,&op
,&&op
,&&&op
, and so on (up to infinity) are implementors of theFn
trait.
So, obviously, increasing the recursion limit wouldn't have helped, and the solutions (using function pointers and &F
s) also fall out fairly immediately from this description.
My main question is this: why can't the trait solver handle this kind of situation? It seems like a really simple case of proof by induction, and my understanding is that the trait solver already has some tools for e.g. coinductive solving. Is there a safety reason that induction cannot be used? Are situations like this too niche to have specific compiler support?