Question Details

No question body available.

Tags

haskell tagless-final

Answers (2)

Accepted Answer Available
Accepted Answer
November 2, 2025 Score: 2 Rep: 52,784 Quality: High Completeness: 60%

As an alternative to @DanielWagner's answer, you could consider using a functional dependency in the Stmt class:

{-# LANGUAGE FunctionalDependencies #-}

class (SynExpr e) => SynStmt e s | s -> e where ^^^^^^^^

This doesn't really reduce the generality. The whole point of Kiselyov's approach is to unambiguously tag an interpreter with a representation type (e.g., for his expressions, String tags a quotation interpreter, Int quotes an evaluation interpreter, etc.).

For your language, where you have both statement and expression representation types floating around, if you have tagged the interpreter using a representation type for your statements, you wouldn't really expect to have an extra degree of freedom to tag a "subinterpreter" that depends on different representation types for expressions, too. Instead, you'd expect the statement representation type to unambiguously determine the expression representation type, and that's all the functional dependency is expressing.

If you find yourself in the situation of wanting to have two interpreters with the same statement representations but different expression representation, you can resolve this with newtypes for the statement representation.

With your original code, modified with only the above functional dependency, it compiles fine, and printstmt st01 is unambiguous, without any type annotations.

November 1, 2025 Score: 5 Rep: 156,250 Quality: Medium Completeness: 60%

It is somewhat annoying that GHC is willing to use the obvious SynExpr instance when checking the type of ex01, but not when checking st01. I think one way to understand what's going on is to imagine that GHC first infers the type of your term, then checks that the inferred type is compatible with the annotated type. (This isn't true, but it's an okay model for some things.) In the ex01 case, there's no ambiguous type because it infers that it needs the SynExpr constraint for the one type variable that exists. But for the st01 case, the SynExpr type variable disappears, and becomes ambiguous during the inference -- it never gets to the stage where it tries to check whether your signature is an okay one.

It's pretty easy to fix, though, just tell it exactly which type you want there:

{-# Language TypeApplications #-} st01 :: forall e s. (SynStmt e s, SynExpr e) => s st01 = qprint @e ({- ... this part is the same ... -})