Ergonomic TTG passes
Motivation
On reddit, @trac-isovector expressed the valid concern that TTG copes quite badly with "minimal" extensions (i.e. concerning only a single extension of HsPar
).
We should offer functions that make that use case less boiler-platey, in particular
- Creating your own pass should not need 100 type family instance declarations when you only add 1 field compared to i.e.
GhcTc
- Converting an AST from parameterised by one phase to an AST parameterised by another should ideally only need to specify the cases where something actually happens. We want "batteries included" experience for the nasty "congruence rules" (
transform (HsApp ex f a) = HsApp ex (transform f) (transform a)
).
Proposal
I don't have a solution yet for 1. (maybe some signature module parameterised on a pass that can be imported for orphan family instances? But then we can't choose to redefine just a fraction of all extension points...).
But for 2. we can just have (only for lambda calculus for illustrative purposes)
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE EmptyDataDecls #-}
module Lib where
type family XApp p :: *
type family XLam p :: *
type family XVar p :: *
type Id = String
data Expr p
= App (XApp p) (Expr p) (Expr p)
| Lam (XLam p) Id (Expr p)
| Var (XVar p) Id
data Vanilla
type instance XApp Vanilla = ()
type instance XLam Vanilla = ()
type instance XVar Vanilla = ()
class Transformer t p1 p2 where
app :: (Expr p1 -> Expr p2) -> XApp p1 -> Expr p1 -> Expr p1 -> Expr p2
default app :: (XApp p1 ~ XApp p2) => (Expr p1 -> Expr p2) -> XApp p1 -> Expr p1 -> Expr p1 -> Expr p2
app t x f a = App x (t f) (t a)
lam :: (Expr p1 -> Expr p2) -> XLam p1 -> Id -> Expr p1 -> Expr p2
default lam :: (XLam p1 ~ XLam p2) => (Expr p1 -> Expr p2) -> XLam p1 -> Id -> Expr p1 -> Expr p2
lam t x id e = Lam x id (t e)
var :: (Expr p1 -> Expr p2) -> XVar p1 -> Id -> Expr p2
default var :: (XVar p1 ~ XVar p2) => (Expr p1 -> Expr p2) -> XVar p1 -> Id -> Expr p2
var _ x id = Var x id
transform :: forall t p1 p2. Transformer t p1 p2 => Expr p1 -> Expr p2
transform (App x f a) = app @t (transform @t) x f a
transform (Lam x id e) = lam @t (transform @t) x id e
transform (Var x id) = var @t @p1 (transform @t) x id
data MyPass
type instance XApp MyPass = ()
type instance XLam MyPass = ()
type instance XVar MyPass = Int
-- case in point: only need to define var to get annotate
data VarLength
instance Transformer VarLength Vanilla MyPass where
var _ _ id = Var (length id) id
annotate :: Expr Vanilla -> Expr MyPass
annotate = transform @VarLength
This will even properly specialise for the particular Transformer
instance, so annotate
is as good as hand-written.
This approach would scale to arbitrarily many extension points.