proarrow-0.1.0.0: test/Examples/SimplyTypedLambdaCalculus.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
module Examples.SimplyTypedLambdaCalculus where
import Control.Applicative (Alternative (..))
import Data.Kind (Constraint, Type)
import Data.Type.Equality ((:~:) (..))
import Prelude hiding (curry, fst, id, snd, (.))
import Test.Falsify.Generator (Function)
import Test.Tasty (TestTree, testGroup)
import Proarrow.Core
( CAT
, CategoryOf (..)
, ObId (..)
, Profunctor (..)
, Promonad (..)
, dimapDefault
, obj
, (//)
, type (+->)
)
import Proarrow.Limit.Terminal (HasTerminalObject (..))
import Proarrow.Category.Monoidal (Monoidal (..), MonoidalProfunctor (..))
import Proarrow.Category.Monoidal.Closed (Closed (..))
import Proarrow.Limit.BinaryProduct
( HasBinaryProducts (..)
, associatorProd
, associatorProdInv
, leftUnitorProd
, leftUnitorProdInv
, rightUnitorProd
, rightUnitorProdInv
)
import Proarrow.Testing
( GenTotal (..)
, MkSomeList (..)
, Some (..)
, SomeProfunctorElt (..)
, Testable (..)
, TestableProfunctor (..)
, TestableType (..)
, TestingEqShow (..)
, eqHask
, genNamed
, genObSuchThat
, genSomeDef
, isGenNonEmpty
, oneElem
, oneOfTotal
)
import Proarrow.Testing.Laws
( testBinaryProducts
, testCategory
, testClosed
, testMonoidal
, testProfunctor
, testTerminalObject
)
import Props.Hask ()
type data TY = K | TY :=> TY
instance ObId K where objId = SK
instance (ObId a, ObId b) => ObId (a :=> b) where objId = SF
type Ty :: CAT TY
data Ty g h where
SK :: Ty K K
SF :: (Ob a, Ob b) => Ty (a :=> b) (a :=> b)
instance Profunctor Ty where
dimap = dimapDefault
r \\ SK = r
r \\ SF = r
instance Promonad Ty where
SK . SK = SK
SF . SF = SF
instance CategoryOf TY where
type (~>) = Ty
type data CON = E | CON :> TY
type Sub :: CAT CON
data Sub g h where
Comp :: Sub h j -> Sub g h -> Sub g j
Empty :: Sub E E
Cons :: Sub g h -> Tm g a -> Sub g (h :> a)
Wk :: (Ob a, Ob g) => Sub (g :> a) g
type Tm :: TY +-> CON
data Tm g a where
Vz :: (Ob a, Ob g) => Tm (g :> a) a
Vs :: (Ob b) => Tm g a -> Tm (g :> b) a
Lam :: (Ob g, Ob a) => Tm (g :> a) b -> Tm g (a :=> b)
App :: forall a b g. (Ob b) => Tm g (a :=> b) -> Tm g a -> Tm g b
instance Profunctor Sub where
dimap = dimapDefault
r \\ Comp f g = r \\ f \\ g
r \\ Empty = r
r \\ Cons f t = r \\ f \\ t
r \\ Wk = r
instance Promonad Sub where
id @a = case sing @a of
SE -> Empty
SC @g' -> Cons (obj @g' . Wk) Vz
Comp f g . h = f . (g . h)
Empty . f = f
Wk @a . r = pComp r
where
pComp :: (Ob g) => Sub h (g :> a) -> Sub h g
pComp (Cons f _) = f
pComp (Comp g h) = case pComp g of
c | c == Comp Wk g -> Comp Wk (Comp g h)
x -> x . h
pComp s = Comp Wk s
Cons f t . r = cons (f . r) (lmap r t)
instance CategoryOf CON where
type (~>) = Sub
type Ob a = (ConOb a)
instance HasTerminalObject CON where
type TerminalObject = E
terminate @a = case sing @a of
SE -> Empty
SC @a' -> terminate @CON @a' . Wk
instance HasBinaryProducts CON where
type n && E = n
type n && (g :> a) = (n && g) :> a
withObProd @a @b r =
case sing @b of
SE -> r
SC @b' -> withObProd @CON @a @b' r
fst @a @b = case sing @b of
SE -> obj @a
SC @b' -> let f = fst @CON @a @b' in f . Wk \\ f
snd @a @b = case sing @b of
SE -> terminate
SC @b' -> let f = snd @CON @a @b' in lift f
(&&&) @_ @_ @y l r =
r // case sing @y of
SE -> l
SC -> cons (l &&& Comp Wk r) (lmap r Vz)
instance MonoidalProfunctor Sub where
one = id
(**) = (***)
instance Monoidal CON where
type a ** b = a && b
type Unit = TerminalObject
withOb2 @a @b = withObProd @_ @a @b
leftUnitor = leftUnitorProd
leftUnitorInv = leftUnitorProdInv
rightUnitor = rightUnitorProd
rightUnitorInv = rightUnitorProdInv
associator @a @b @c = associatorProd @a @b @c
associatorInv @a @b @c = associatorProdInv @a @b @c
type family Exp (g :: CON) (a :: TY) :: CON where
Exp E _ = E
Exp (g :> b) a = Exp g a :> (a :=> b)
withObExpSub :: forall g a r. (Ob g, Ob a) => ((Ob (Exp g a)) => r) -> r
withObExpSub r = case sing @g of
SE -> r
SC @g' -> withObExpSub @g' @a r
currySub :: forall a d g. (Ob g, Ob a) => Sub (g :> a) d -> Sub g (Exp d a)
currySub s =
s // case sing @d of
SE -> terminate
SC -> case uncons s of (s1, t) -> cons (currySub s1) (Lam t)
uncurrySub :: forall a d g. (Ob d, Ob a) => Sub g (Exp d a) -> Sub (g :> a) d
uncurrySub s =
s // case sing @d of
SE -> terminate
SC @g' -> withObExpSub @g' @a $ case uncons s of (s', t) -> cons (uncurrySub s') (App (Vs t) Vz)
instance Closed CON where
type E ~~> d = d
type (g :> a) ~~> d = g ~~> Exp d a
withObExp @g @d r = case sing @g of
SE -> r
SC @g' @a -> withObExpSub @d @a $ withObExp @CON @g' @(Exp d a) r
curry @d @g f =
f // case sing @g of
SE -> f
SC @g' -> withObProd @CON @d @g' (curry @CON @d @g' (currySub f))
apply @d @g = case sing @d of
SE -> id
SC @d' @a -> withObExpSub @g @a $ uncurrySub (apply @CON @d' @(Exp g a))
class (E && a ~ a, (c && b) && a ~ c && (b && a)) => Rules a b c
instance (E && a ~ a, (c && b) && a ~ c && (b && a)) => Rules a b c
type SingCon :: CON -> Type
data SingCon g where
SE :: SingCon E
SC :: (Ob g, Ob a) => SingCon (g :> a)
class (forall b c. Rules g b c) => ConOb g where sing :: SingCon g
instance ConOb E where sing = SE
instance (Ob g, Ob a) => ConOb (g :> a) where sing = SC
instance Profunctor Tm where
lmap (Comp l r) t = lmap r (lmap l t)
lmap (Cons _ t) Vz = t
lmap (Cons f _) (Vs t) = lmap f t
lmap l (Lam t) = Lam (lmap (cons (l . Wk) Vz) t) \\ l
lmap l (App s t) = App (lmap l s) (lmap l t)
lmap Wk t = Vs t
rmap SK = id
rmap SF = id
r \\ Vz = r
r \\ Vs f = r \\ f
r \\ Lam t = r \\ t
r \\ App t _ = r \\ t
uncons :: forall g d a. (Ob d, Ob a) => Sub g (d :> a) -> (Sub g d, Tm g a)
uncons (Cons s t) = (s, t)
uncons (Comp l r) = case uncons l of
(s, t) -> (s . r, lmap r t)
uncons Wk = (Wk . Wk, Vs Vz)
lift :: (Ob a) => Sub d g -> Sub (d :> a) (g :> a)
lift s = Cons (s . Wk) Vz \\ s
weakenR :: (Ob a) => Sub d g -> Sub (d && a) (g && a)
weakenR @a f =
f // case sing @a of
SE -> f
SC @a' @x -> lift @x (weakenR @a' f)
-- simplifies @Cons (Wk . σ) (lmap σ Vz)@ to @σ@
cons :: Sub d g -> Tm d a -> Sub d (g :> a)
cons (Comp Wk s) t = case (countWk s, countVs t) of
(Just l, Just r) | l == r -> case uncons s of (s', _) -> Cons s' t
_ -> s // Cons (Comp Wk s) t
cons s t = Cons s t
countWk :: Sub a b -> Maybe Int
countWk Wk = Just 1
countWk (Comp Wk w) = (+ 1) <$> countWk w
countWk _ = Nothing
countVs :: Tm g a -> Maybe Int
countVs Vz = Just 0
countVs (Vs t) = (+ 1) <$> countVs t
countVs _ = Nothing
app :: (Ob b) => Tm g (a :=> b) -> Tm g a -> Tm g b
app (Lam t) u = lmap (cons id u) t \\ u
app t u = App t u
lam :: (Ob g, Ob a) => (SingCon g -> Tm (g :> a) b) -> Tm g (a :=> b)
lam f = Lam (f sing)
type family EvalTy (a :: TY) :: Type where
EvalTy K = Bool
EvalTy (a :=> b) = EvalTy a -> EvalTy b
type family EvalCon (g :: CON) :: Type where
EvalCon E = ()
EvalCon (g :> a) = (EvalCon g, EvalTy a)
eval :: (Ob a) => Tm g a -> EvalCon g -> EvalTy a
eval Vz (_, x) = x
eval (Vs f) (g, _) = eval f g
eval (Lam f) g = \x -> eval f (g, x) \\ f
eval (App t u) g = eval t g (eval u g) \\ u
type Nat a = (a :=> a) :=> (a :=> a)
-- \f z -> z
nat0 :: (Ob a) => Tm E (Nat a)
nat0 = Lam (Lam Vz)
-- \f z -> f z
nat1 :: (Ob a) => Tm E (Nat a)
nat1 = Lam (Lam (App (Vs Vz) Vz))
-- \n f z -> f (n f z)
succ :: (Ob a) => Tm E (Nat a :=> Nat a)
succ = Lam (Lam (Lam (App (Vs Vz) (App (App (Vs (Vs Vz)) (Vs Vz)) Vz))))
-- * Testing
--
-- The generators follow the Props.Free recipe: total, type-directed generation via
-- 'GenTotal' ('empty' for uninhabited branches, so nothing is ever discarded), structural
-- recursion wherever a branch shrinks the goal, and a small fixed palette of intermediate
-- objects for the one branch that doesn't ('App'/composition), bounded by fuel. Equality is
-- semantic (terms and substitutions are compared through 'eval'/'evalSub'), so the smart
-- normalizing constructors don't have to be confluent for the laws to pass.
-- ** Structural equality and display, needing only 'Ob'
eqTy :: forall (x :: TY) (y :: TY). (Ob x, Ob y) => Maybe (x :~: y)
eqTy = case (obj @x, obj @y) of
(SK, SK) -> Just Refl
(SF @a @b, SF @a' @b') -> case (eqTy @a @a', eqTy @b @b') of
(Just Refl, Just Refl) -> Just Refl
_ -> Nothing
_ -> Nothing
showTy :: forall (a :: TY). (Ob a) => String
showTy = case obj @a of
SK -> "K"
SF @a' @b' -> "(" ++ showTy @a' ++ " => " ++ showTy @b' ++ ")"
eqCon :: forall (g :: CON) (h :: CON). (Ob g, Ob h) => Maybe (g :~: h)
eqCon = case (sing @g, sing @h) of
(SE, SE) -> Just Refl
(SC @g' @a, SC @h' @b) -> case (eqTy @a @b, eqCon @g' @h') of
(Just Refl, Just Refl) -> Just Refl
_ -> Nothing
_ -> Nothing
showCon :: forall (g :: CON). (Ob g) => String
showCon = case sing @g of
SE -> "E"
SC @g' @a -> "(" ++ showCon @g' ++ " :> " ++ showTy @a ++ ")"
-- ** Semantic interpretation of substitutions (terms already have 'eval')
evalSub :: Sub g h -> EvalCon g -> EvalCon h
evalSub Empty () = ()
evalSub Wk (g, _) = g
evalSub (Cons s t) g = (evalSub s g, eval t g \\ t)
evalSub (Comp f g) x = evalSub f (evalSub g x)
-- ** Generateability of interpreted values
--
-- 'eqHask' compares interpreted terms by sampling arguments, which needs falsify 'Function'
-- instances at every arrow's /left/ argument. These families thread that requirement through
-- 'TestOb', as @KnownFree@ does in Props.Free. The palettes below only ever put 'K'
-- on the left of an arrow (and only 'K' entries in contexts), so the vacuous
-- @Function (a -> b)@ instance is never exercised.
type family TyTestOb (a :: TY) :: Constraint where
TyTestOb K = ()
TyTestOb (a :=> b) = (Function (EvalTy a), TyTestOb a, TyTestOb b)
type family ConTestOb (g :: CON) :: Constraint where
ConTestOb E = ()
ConTestOb (g :> a) = (ConTestOb g, TyTestOb a, Function (EvalTy a))
withEvalTy :: forall (a :: TY) r. (Ob a, TyTestOb a) => ((TestableType (EvalTy a), TestingEqShow (EvalTy a)) => r) -> r
withEvalTy r = case obj @a of
SK -> r
SF @x @y -> withEvalTy @x (withEvalTy @y r)
withEvalCon
:: forall (g :: CON) r. (Ob g, ConTestOb g) => ((TestableType (EvalCon g), TestingEqShow (EvalCon g)) => r) -> r
withEvalCon r = case sing @g of
SE -> r
SC @g' @a -> withEvalCon @g' (withEvalTy @a r)
-- ** 'TestOb' is closed under the categorical structure
withTestObProdCON :: forall (a :: CON) b r. (TestOb a, TestOb b) => ((TestOb (a && b)) => r) -> r
withTestObProdCON r = case sing @b of
SE -> r
SC @b' -> withTestObProdCON @a @b' r
-- | @'TestOb' ('Exp' d a)@ for an entry type @a@ of a testable context.
withTestObExpArg
:: forall (d :: CON) a r. (TestOb d, Ob a, TyTestOb a, Function (EvalTy a)) => ((TestOb (Exp d a)) => r) -> r
withTestObExpArg r = case sing @d of
SE -> r
SC @d' -> withTestObExpArg @d' @a r
withTestObExpCON :: forall (g :: CON) d r. (TestOb g, TestOb d) => ((TestOb (g ~~> d)) => r) -> r
withTestObExpCON r = case sing @g of
SE -> r
SC @g' @a -> withTestObExpArg @d @a (withTestObExpCON @g' @(Exp d a) r)
-- ** Object palettes
type TyPalette = '[K, K :=> K, K :=> (K :=> K)]
tyPalette :: [Some TY]
tyPalette = mkSomeList @TY @TyPalette
type ConPalette = '[E, E :> K, (E :> K) :> K]
conPalette :: [Some CON]
conPalette = mkSomeList @CON @ConPalette
-- ** Total type-directed generators
-- | Generate a term of type @a@ in context @g@. The variable branch recurses structurally on
-- the context and the lambda branch on the type, so both terminate on their own; only 'App'
-- needs an intermediate type (drawn from 'tyPalette') and is bounded by fuel. Uninhabited
-- goals (e.g. @Tm E K@) come out 'empty' instead of looping or discarding.
genTm :: forall (g :: CON) (a :: TY). (Ob g, Ob a) => Int -> GenTotal (Tm g a)
genTm fuel = oneOfTotal [varB, lamB, appB]
where
varB = case sing @g of
SE -> empty
SC @g' @b ->
oneOfTotal
[ case eqTy @b @a of Just Refl -> pure Vz; Nothing -> empty
, Vs <$> genTm @g' @a fuel
]
lamB = case obj @a of
SK -> empty
SF @a1 @a2 -> Lam <$> genTm @(g :> a1) @a2 fuel
appB
| fuel <= 0 = empty
| otherwise =
oneOfTotal
[ App <$> genTm @g @(c :=> a) (fuel - 1) <*> genTm @g @c (fuel - 1)
| Some @c <- tyPalette
]
-- | Generate a substitution, type-directed on both contexts: identity and weakening when the
-- shapes allow it, 'terminate' into the empty context, 'cons' peeling the target context (which
-- terminates structurally), and fuel-bounded composition through 'conPalette'.
genSub :: forall (g :: CON) (h :: CON). (Ob g, Ob h) => Int -> GenTotal (Sub g h)
genSub fuel = oneOfTotal [idB, termB, wkB, consB, compB]
where
idB = case eqCon @g @h of Just Refl -> pure id; Nothing -> empty
termB = case sing @h of SE -> pure terminate; SC -> empty
wkB = case sing @g of
SE -> empty
SC @g' -> (. Wk) <$> genSub @g' @h fuel
consB = case sing @h of
SE -> empty
SC @h' @a -> cons <$> genSub @g @h' fuel <*> genTm @g @a fuel
compB
| fuel <= 0 = empty
| otherwise =
oneOfTotal
[ (.) <$> genSub @m @h (fuel - 1) <*> genSub @g @m (fuel - 1)
| Some @m <- conPalette
]
-- ** Testable instances
instance Testable TY where
type TestOb a = (ObId a, TyTestOb a)
showOb @a = showTy @a
genSome = genSomeDef @TyPalette
deriving instance Show (Ty a b)
deriving instance Eq (Ty a b)
instance (Ob a, Ob b) => TestingEqShow (Ty a b)
instance (Ob a, Ob b) => TestableType (Ty a b) where
gen = case eqTy @a @b of
Just Refl -> oneElem (obj @a)
Nothing -> GenEmpty (error "gen @Ty")
instance TestableProfunctor Ty
instance Testable CON where
type TestOb g = (ConOb g, ConTestOb g)
showOb @g = showCon @g
genSome = genSomeDef @ConPalette
deriving instance Show (Sub a b)
-- | Structural equality, used by the normalizing smart constructors ('cons', 'pComp'). The
-- test suite compares substitutions semantically instead, see 'TestingEqShow'.
instance Eq (Sub a b) where
Empty == Empty = True
Wk == Wk = True
Cons a b == Cons c d = a == c && b == d
Comp @l a b == Comp @r c d =
a // c // case eqCon @l @r of
Just Refl -> a == c && b == d
Nothing -> False
_ == _ = False
instance (Ob a, ConTestOb a, Ob b, ConTestOb b) => TestingEqShow (Sub a b) where
eqP l r = withEvalCon @a (withEvalCon @b (eqHask (evalSub l) (evalSub r)))
showP = show
instance (Ob a, ConTestOb a, Ob b, ConTestOb b) => TestableType (Sub a b) where
gen = genSub @a @b 3
instance TestableProfunctor Sub where
genProfunctorElt nm = do
Some @g <- genObSuchThat @CON \(Some @g') -> any (\(Some @h') -> isGenNonEmpty @(Sub g' h')) conPalette
Some @h <- genObSuchThat @CON \(Some @h') -> isGenNonEmpty @(Sub g h')
s <- genNamed @(Sub g h) nm
pure (SomeP s)
deriving instance Show (Tm g a)
-- | Structural equality, only used by @Eq Sub@ above.
instance Eq (Tm g a) where
Vz == Vz = True
Lam l == Lam r = l == r
App @al fl xl == App @ar fr xr =
xl // xr // case eqTy @al @ar of
Just Refl -> xl == xr && fl == fr
Nothing -> False
Vs tl == Vs tr = tl == tr
_ == _ = False
instance (Ob g, ConTestOb g, Ob a, TyTestOb a) => TestingEqShow (Tm g a) where
eqP l r = withEvalCon @g (withEvalTy @a (eqHask (eval l) (eval r)))
showP = show
instance (Ob g, ConTestOb g, Ob a, TyTestOb a) => TestableType (Tm g a) where
gen = genTm @g @a 3
instance TestableProfunctor Tm where
genProfunctorElt nm = do
Some @g <- genObSuchThat @CON \(Some @g') -> any (\(Some @a') -> isGenNonEmpty @(Tm g' a')) tyPalette
Some @a <- genObSuchThat @TY \(Some @a') -> isGenNonEmpty @(Tm g a')
t <- genNamed @(Tm g a) nm
pure (SomeP t)
test :: TestTree
test =
testGroup
"Simply typed lambda calculus"
[ testCategory @CON
, testTerminalObject @CON
, testBinaryProducts @CON (\ @a @b r -> withTestObProdCON @a @b r)
, testMonoidal @CON (\ @a @b r -> withTestObProdCON @a @b r)
, testClosed @CON (\ @a @b r -> withTestObProdCON @a @b r) (\ @a @b r -> withTestObExpCON @a @b r)
, testGroup "Tm profunctor" [testProfunctor @Tm]
]