packages feed

bound-extras-0: examples/SystemF.hs

{-# LANGUAGE DeriveFoldable        #-}
{-# LANGUAGE DeriveFunctor         #-}
{-# LANGUAGE DeriveTraversable     #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings     #-}
module SystemF (tests) where

import Bound.Class          ((>>>=))
import Bound.Scope
import Bound.ScopeH
import Bound.Var            (Var (..))
import Control.Monad        (ap)
import Control.Monad.Module
import Data.Bifoldable      (Bifoldable (..))
import Data.Bifunctor       (Bifunctor (..))
import Data.Bitraversable   (Bitraversable (..), bifoldMapDefault, bimapDefault)
import Data.Functor.Classes (Eq1 (..), eq1)
import Data.String          (IsString (..))
import System.FilePath      ((-<.>), (</>))
import Test.Tasty           (TestTree, testGroup)
import Test.Tasty.Golden    (goldenVsString)

import qualified Data.ByteString.Lazy.UTF8 as UTF8

import Pretty

-------------------------------------------------------------------------------
-- Types
-------------------------------------------------------------------------------

-- | Types.
--
-- Noteworthy thing is the absence of any /application/.
-- 'TForall' abstract only over types, not type-constructors;
-- so we don't have type application either.
-- In short: everything is well-kinded by construction.
data Ty a
    = TV a
    | Ty a :-> Ty a
    | TForall (Scope () Ty a)
  deriving (Functor, Foldable, Traversable)

infixr 1 :->

instance Applicative Ty where
    pure = TV
    (<*>) = ap

instance Monad Ty where
    return = TV
    TV x      >>= k = k x
    (a :-> b) >>= k = (a >>= k) :-> (b >>= k)
    TForall t >>= k = TForall (t >>>= k)

instance Eq1 Ty where
    liftEq eq (TV a)      (TV a')      = eq a a'
    liftEq eq (a :-> b)   (a' :-> b')  = liftEq eq a a' && liftEq eq b b'
    liftEq eq (TForall a) (TForall a') = liftEq eq a a'

    liftEq _ TV {} _ = False
    liftEq _ (:->) {} _ = False
    liftEq _ TForall {} _ = False

instance Eq a => Eq (Ty a) where (==) = eq1

instance IsString a => IsString (Ty a) where
    fromString = TV . fromString

forall_ :: Eq a => a -> Ty a -> Ty a
forall_ n t = TForall (abstract1 n t)

-------------------------------------------------------------------------------
-- Expression
-------------------------------------------------------------------------------

data Expr b a
    = V a

    -- term abstraction
    | Lam (Ty b) (Scope () (Expr b) a)
    | App (Expr b a) (Expr b a)

    -- type abstraction
    | TyApp (Expr b a) (Ty b)
    | Forall (ScopeH () (Expr' a) Ty b)

instance IsString a => IsString (Expr b a) where
    fromString = V . fromString

{-
-- | In practice we should write Bitraversable instance, and use
-- 'bimapDefault' as Bifunctor implementation.
--
-- That's an argument (not a good one) to omit 'bimapScope' from @bound@.
instance Bifunctor Expr where
    bimap f g = go where
        go (V x) = V (g x)
        go (App a b) = App (go a) (go b)
        go (TyApp a b) = TyApp (go a) (fmap f b)
        go (Lam t b) = Lam (fmap f t) (bimapScope f g b)
        go (Forall (ScopeH b)) = Forall $ ScopeH $
            bimap g (fmap (fmap f)) b
-}

instance Bifunctor  Expr  where bimap     = bimapDefault
instance Bifunctor  Expr' where bimap     = bimapDefault
instance Bifoldable Expr  where bifoldMap = bifoldMapDefault
instance Bifoldable Expr' where bifoldMap = bifoldMapDefault

instance Bitraversable Expr' where
    bitraverse f g = fmap Expr' . bitraverse g f . unExpr'

instance Bitraversable Expr where
    bitraverse f g = go where
        go (V x)       = V <$> g x
        go (App a b)   = App <$> go a <*> go b
        go (TyApp a b) = TyApp <$> go a <*> traverse f b
        go (Lam t b)   = Lam <$> traverse f t <*> bitraverseScope f g b
        go (Forall s)  = Forall <$> bitransverseScopeH (bitraverse g) traverse f s

instance Functor (Expr b) where
    fmap = second

instance Applicative (Expr b) where
    pure = V
    (<*>) = ap

instance Monad (Expr b) where
    return = V

    V x       >>= k = k x
    App a b   >>= k = App (a >>= k) (b >>= k)
    Lam t b   >>= k = Lam t (b >>>= k)
    TyApp a b >>= k = TyApp (a >>= k) b
    Forall b  >>= k = Forall $ ScopeH $ overExpr' (>>= k') $ unscopeH b where
        k' = first (F . TV) . k

-- | @'Flip' 'Expr'@.
newtype Expr' a b = Expr' { unExpr' :: Expr b a }

overExpr :: (Expr' a b -> Expr' a b') -> Expr b a -> Expr b' a
overExpr f = unExpr' . f . Expr'

overExpr' :: (Expr b a -> Expr b' a') -> Expr' a b -> Expr' a' b'
overExpr' f = Expr' . f . unExpr'

instance Functor (Expr' a) where
    fmap f = overExpr' (first f)

instance Module (Expr' c) Ty where
    Expr' (V a) >>== _ = Expr' (V a)

    Expr' (Lam t s) >>== k = Expr' $ Lam (t >>= k) $ hoistScope (overExpr (>>== k)) s
    Expr' (Forall s) >>== k = Expr' $ Forall $ s >>== k

    Expr' (App a b) >>== k = Expr' $ App
        (unExpr' (Expr' a >>== k))
        (unExpr' (Expr' b >>== k))

    Expr' (TyApp a b) >>== k = Expr' $ TyApp
        (unExpr' (Expr' a >>== k))
        (b >>= k)

tyLam_ :: Eq b => b -> Expr b a -> Expr b a
tyLam_ n e = Forall $ abstract1H n (Expr' e)

lam_ :: Eq a => a -> Ty b -> Expr b a -> Expr b a
lam_ x t b = Lam t (abstract1 x b)

-------------------------------------------------------------------------------
-- Normal form
-------------------------------------------------------------------------------

tnf :: Ty b -> Ty b
tnf (TV x) = TV x
tnf (a :-> b) = tnf a :-> tnf b
tnf (TForall a) = TForall (toScope $ tnf $ fromScope a)

nf :: Expr b a -> Expr b a
nf (V x) = V x
nf (Lam t b) = Lam (tnf t) (toScope $ nf $ fromScope b)
nf (App a b) = case nf a of
    Lam _ a' -> nf $ instantiate1 b a'
    a'       -> App a' (nf b)
nf (Forall e) = Forall $ toScopeH $ overExpr' nf $ fromScopeH e
nf (TyApp a b) = case nf a of
    Forall a' -> nf $ unExpr' $ instantiate1H b a'
    a'        -> TyApp a' (tnf b)

-------------------------------------------------------------------------------
-- Pretty
-------------------------------------------------------------------------------

instance Pretty a => Pretty (Ty a) where
    ppr x = traverse ppr x >>= pprTy

pprTy :: Ty Doc -> PrettyM Doc
pprTy (TV x)      = return x
pprTy (a :-> b)   = sexpr (text "->") <$> traverse pprTy [a, b]
pprTy (TForall s) = do
    a <- text <$> fresh "a"
    pprTy (instantiate1 (TV a) s)

instance (Pretty a, Pretty b) => Pretty (Expr b a) where
    ppr x = bitraverse ppr ppr x >>= pprExpr

pprExpr :: Expr Doc Doc -> PrettyM Doc
pprExpr (V x)       = return x
pprExpr (App a b)   = pprApplications $ applications a ++ [Right b]
pprExpr (TyApp a b) = pprApplications $ applications a ++ [Left b]
pprExpr (Lam t b)   = do
    x <- text <$> fresh "x"
    t' <- pprTy t
    b' <- pprExpr $ instantiate1 (V x) b
    return $ sexpr (text "fn") [ sexpr "the" [t', x], b']
pprExpr (Forall b) = do
    t <- text <$> fresh "b"
    b' <- pprExpr $ unExpr' $ instantiate1H (TV t) b
    return $ sexpr (text "poly") [ t , b']

pprApplications :: [Either (Ty Doc) (Expr Doc Doc)] -> PrettyM Doc
pprApplications []       = return $ text "()"
pprApplications (x : xs) = sexpr <$> pp x <*> traverse pp xs
  where
    pp = either pprTy pprExpr

-- We output
--   (0 1 2 3)
-- instead of
--   (((0 1) 2) 3)
-- small, but nice improvement!
applications :: Expr a b -> [Either (Ty a) (Expr a b)]
applications (App a b)   = applications a ++ [Right b]
applications (TyApp a b) = applications a ++ [Left b]
applications e           = [Right e]

-------------------------------------------------------------------------------
-- Applications
-------------------------------------------------------------------------------

infixl 2 $$, @@

($$) :: Expr b a -> Expr b a -> Expr b a
($$) = App

(@@) :: Expr b a -> Ty b -> Expr b a
(@@) = TyApp

-------------------------------------------------------------------------------
-- Type-checking
-------------------------------------------------------------------------------

-- | Type-check assuming that free variables have the similarly named type.
-- In systemf type and term namespaces are different!
check :: Eq a => Expr a a -> Maybe (Ty a)
check = check' . fmap TV

-- No error reporting :)
check' :: Eq a => Expr a (Ty a) -> Maybe (Ty a)
check' (V a) = Just a
check' (App f x) = do
    f' <- check' f
    x' <- check' x
    case f' of
        a' :-> b' | a' == x' -> return b'
        _                    -> Nothing
check' (TyApp x t) = do
    x' <- check' x
    case x' of
        TForall b -> return (instantiate1 t b)
        _         -> Nothing
check' (Lam t b) = do
    b' <- check' (instantiate1 (V t) b)
    return (t :-> b')

check' (Forall b) = do
    let b' = unExpr' $ fromScopeH b
    b'' <- check' (fmap (fmap F) b')
    return $ TForall $ toScope b''

-------------------------------------------------------------------------------
-- Identity function
-------------------------------------------------------------------------------

-- idType_ :: Ty ShortText
-- idType_ = forall_ "n" $ "n" :-> "n"

id_ :: Expr ShortText ShortText
id_ = tyLam_ "a" $ lam_ "x" "a" "x"

-------------------------------------------------------------------------------
-- Church numerals
-------------------------------------------------------------------------------

natType :: Ty ShortText
natType = forall_ "a" $ ("a" :-> "a") :-> "a" :-> "a"

zero :: Expr ShortText ShortText
zero = tyLam_ "a" $ lam_ "f" ("a" :-> "a") $ lam_ "z" "a" "z"

-- sucType :: Ty ShortText
-- sucType = natType :-> natType

suc :: Expr ShortText ShortText
suc
    = lam_ "n" natType
    $ tyLam_ "a" $ lam_ "f" ("a" :-> "a") $ lam_ "z" "a"
    $ "n" @@ "a" $$ "f" $$ ("f" $$ "z")

demo :: String -> Expr ShortText ShortText -> [String]
demo name e = case check e of
    Nothing ->
        [ name ++ " = " ++ pretty e
        , "DOESN'T TYPECHECK"
        ]
    Just t ->
        [ name ++ " : " ++ pretty t
        , name ++ " = " ++ pretty e
        , name ++ " = " ++ pretty (nf e)
        ]

tests :: TestTree
tests = testGroup "System F"
    [ demo' "id" id_
    , demo' "0" zero
    , demo' "suc" suc
    , demo' "1" (suc $$ zero)
    , demo' "2" (suc $$ (suc $$ zero))
    ]
  where
    demo' name e = goldenVsString name ("examples" </> name' -<.> "txt")
        $ return $ UTF8.fromString $ unlines
        $ demo name e
      where
        name' = "sysf-" ++ name