The recursion-schemes perspective is to view recursive types as fixed points of functors. The type of expressions is the fixed point of the following functor:
data ExprF expr = Const Int
| Add expr expr
The point of changing the name of the variable is to make explicit the fact that it is a placeholder for the actual type of expressions, that would otherwise be defined as:
data Expr = Const Int | Add Expr Expr
In Stmt
, there are two recursive types, Expr
and Stmt
itself. So we put two holes/unknowns.
data StmtF expr stmt = Compound [stmt]
| Print expr
When we take a fixpoint with Fix
or Cofree
, we are now solving a system of two equations (one for Expr
, one for Stmt
), and that comes with some amount of boilerplate.
Instead of applying Fix
or Cofree
directly, we generalize, taking those fixpoint combinators (Fix
, Cofree
, Free
) as parameters in the construction of expressions and statements:
type Expr_ f = f ExprF
type Stmt_ f = f (StmtF (Expr_ f))
Now we can say Expr_ Fix
or Stmt_ Fix
for the unannotated trees, and Expr_ (Flip Cofree ann)
, Stmt_ (Flip Cofree ann)
. Unfortunately we have to pay another LOC fee to make the kinds match, and the types get ever more convoluted.
newtype Flip cofree a f b = Flip (cofree f a b)
(This also assumes we want to use the same Fix
or Cofree
everywhere at the same times.)
Another representation to consider is (called HKD nowadays):
data Expr f = Const Int
| Add (f Expr) (f Expr)
data Stmt f = Compount [f Stmt]
| Print (f (Expr f))
where you only abstract from annotation/no-annotation (f = Identity
or (,) ann
) and not from recursion.