From this Haskell Cafe post, and borrowing some code examples from jyp, we can construct a simple PHOAS evaluator in Haskell as:
{-# LANGUAGE GADTs #-}
{-# LANGUAGE RankNTypes #-}
import Data.Char
data Term v t where
Var :: v t -> Term v t
App :: Term v (a -> b) -> Term v a -> Term v b
Lam :: (v a -> Term v b) -> Term v (a -> b)
data Exp t = Exp (forall v. Term v t)
-- An evaluator
eval :: Exp t -> t
eval (Exp e) = evalP e
data Id a = Id {fromId :: a}
evalP :: Term Id t -> t
evalP (Var (Id a)) = a
evalP (App e1 e2) = evalP e1 $ evalP e2
evalP (Lam f) = \a -> evalP (f (Id a))
data K t a = K t
showTermGo :: Int -> Term (K Int) t -> String
showTermGo _ (Var (K i)) = "x" ++ show i
showTermGo d (App f x) = "(" ++ showTermGo d f ++ " " ++ showTermGo d x ++ ")"
showTermGo d (Lam a) = "@x" ++ show d ++ " " ++ showTermGo (d+1) (a (K d))
showTerm :: Exp t -> String
showTerm (Exp e) = showTermGo 0 e
This implementation allows us to create, normalize and stringify λ-calculus terms. The problem is, eval has type Exp t -> t rather than Exp t -> Exp t. As such, it isn't clear to me how to evaluate a term to normal form, and then stringify it. Is that possible with PHOAS?
Let's start by trying the most naive thing:
We get stuck at that hole because we need a function
Term v a -> v a, so now we know that we should chose thevsuch that it containsTerm v. We can chosev ~ Term v, but you can't use recursive types directly like that so you need to create a new data type:(I believe the
FixTermtype is isomorphic to the non-parametric HOAS type.)Now we can use that to define our evaluation function:
This works, but unfortunately we can't recover the original
Term v afrom this. It's easy to see that because it never produces aVarconstructor. We can again try and see where we get stuck:This time we need a function
v a -> FixTerm a. To be able to do that we can add a case to theFixTermdata type, which is reminiscent of the free monad type:Now we can define the top-level eval: