packages feed

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]
    ]