I've been fiddling around with the Elm compiler, which is written in Haskell.

I'd like to start implementing some optimizations for it, and part of this involves traversing the AST and adding "annotation" to certain nodes, such as tail-calls, etc.

I know I can use SYB or uniplate to do the traversal, but I'm wondering if there's a boilerplate-free way to deal with the types.

So, suppose we have a bunch of algebraic types for our AST:

data Expr = PlusExpr Expr Expr ... data Def = TypeAlias String [String] Type ...

If I were writing the boilerplate, I'd make new types like this:

data AnnotatedExpr = PlusExpr Expr Expr [Annotation] ... data AnnotatedDef = TypeAlias String [String] Type [Annotation] ...

This is a lot of boilderplate to write, and it seems like good practice to avoid this.

I could write something like this:

Data AnnotationTree = Leaf [Annotation] | Internal [AnnotationTree] [Annotation]

Then I'd just have an annotation tree running parallel to the AST. But there is no guarantee that these trees will have the same structure, so we lose type safety.

So I'm wondering, is there an elegant/recommended solution to avoid boilerplate but still annotate a tree in a type-safe way? To replace each node with an equivalent one, plus a list of annotations which will be used in compilation later?