packages feed

sylvia-0.2.0.1: Sylvia/Model.hs

{-# LANGUAGE Rank2Types #-}

-- |
-- Module      : Sylvia.Model
-- Copyright   : GPLv3
--
-- Maintainer  : chrisyco@gmail.com
-- Portability : portable (Rank2Types)
--
-- The 'Exp' data type, for representing lambda expressions using De Bruijn
-- indices, plus basic abstraction and application operations.

module Sylvia.Model
    (
    -- * Types
      Inc(..)
    , Exp(..)

    -- * Conversions
    , verify
    , verify'

    -- * Abstracting
    , abstract
    , match
    , shiftUp

    -- * Applying
    , apply
    , subst
    , shiftDown

    -- * Simplifying
    , evalHNF
    ) where

import Control.Applicative ( Applicative(..) )
import Control.Monad ( ap )
import Data.Foldable ( Foldable(foldMap) )
import Data.Functor ( (<$>) )
import Data.Monoid ( Monoid(..) )
import Data.Traversable ( Traversable(traverse) )
import Data.Void ( Void )

-- | A structure for representing whole numbers.
--
-- The problem with referring to variables with plain integers is that
-- it's impossible to tell whether an expression refers to any free
-- variables. 'Inc' solves this by letting the type dictate the maximum
-- value of the number.
--
-- For example, 3 would be represented as @S (S (S x))@, where @x@ can
-- be any value. Since it is nested three levels deep, it would have the
-- type @Inc (Inc (Inc a))@.
data Inc a
    = O   -- ^ Zero
    | S a -- ^ Add one
  deriving (Eq, Ord, Read, Show)

instance Functor Inc where
    fmap = mapI

instance Applicative Inc where
    pure = return
    (<*>) = ap

instance Monad Inc where
    return = S
    val >>= f = joinI (mapI f val)

instance Foldable Inc where
    foldMap _ O = mempty
    foldMap f (S x) = f x

instance Traversable Inc where
    traverse _ O = pure O
    traverse f (S x) = S <$> f x

-- | Apply a function to the value inside the 'Inc', if it has one.
mapI :: (a -> b) -> Inc a -> Inc b
mapI _ O = O
mapI f (S x) = S (f x)

-- | Remove one layer of nesting, projecting the inner value onto the outside.
joinI :: Inc (Inc a) -> Inc a
joinI (S (S x)) = S x
joinI _ = O

-- | A lambda expression.
data Exp a
    -- | Variable reference.
    = Ref a
    -- | Lambda abstraction.
    -- Since the function introduces a variable, the expression inside
    -- the lambda would have one more variable than the expression
    -- outside, hence the extra 'Inc'.
    | Lam (Exp (Inc a))
    -- | Function application.
    | App (Exp a) (Exp a)
  deriving (Eq, Ord, Read, Show)

instance Functor Exp where
    fmap = mapE

instance Applicative Exp where
    pure = return
    (<*>) = ap

instance Monad Exp where
    return = Ref
    e >>= f = joinE (mapE f e)

instance Foldable Exp where
    foldMap f e = case e of
        Ref x   -> f x
        Lam e'  -> foldMap (foldMap f) e'
        App a b -> foldMap f a `mappend` foldMap f b

instance Traversable Exp where
    traverse f e = case e of
        Ref x   -> Ref <$> f x
        Lam e'  -> Lam <$> traverse (traverse f) e'
        App a b -> App <$> traverse f a <*> traverse f b

-- | Apply a function to every leaf value in the tree.
mapE :: (a -> b) -> Exp a -> Exp b
mapE f e = case e of
    Ref x   -> Ref (f x)
    Lam e'  -> Lam (mapE (mapI f) e')
    App a b -> App (mapE f a) (mapE f b)

-- | Flatten a nested expression by gluing its nodes onto the main tree.
joinE :: Exp (Exp a) -> Exp a
joinE e = case e of
    Ref x   -> x
    Lam e'  -> Lam (joinE (mapE distE e'))
    App a b -> App (joinE a) (joinE b)

-- | Convert a wrapped-up expression into an expression with wrapped-up
-- references.
distE :: Inc (Exp a) -> Exp (Inc a)
distE O = Ref O
distE (S x) = mapE S x

-- | Check there are no references to free variables in the given
-- expression. If there are none, return Just the expression; otherwise,
-- return Nothing.
verify :: Exp a -> Maybe (Exp Void)
verify = traverse (const Nothing)

-- | Like 'verify', but chucks an error instead of returning Nothing.
verify' :: Exp a -> Exp Void
verify' e = case verify e of
    Just res -> res
    Nothing  -> error "Sylvia.Model.verify': invalid expression"

-- | Create a lambda abstraction.
--
-- If you are using variable names as identifiers, use:
--
-- > abstract (match "foo")
--
-- If you are using de Bruijn indices, use:
--
-- > abstract shiftUp
abstract
    :: (a -> Inc a) -- ^ Matching function
    -> Exp a        -- ^ Function body
    -> Exp a        -- ^ Result
abstract f = Lam . mapE f

match
    :: Eq a
    => a     -- ^ Value to replace
    -> a     -- ^ Value to check
    -> Inc a -- ^ Result
match x y = if x == y then O else S y

-- | Add one layer of 'Inc', decrementing the index inside.
--
-- This is the inverse of 'shiftDown'.
shiftUp
    :: Integral a
    => a     -- ^ Index to check
    -> Inc a -- ^ Result
shiftUp index
  | index >  0 = S (index - 1)
  | index == 0 = O
  | otherwise = error "matchIndex: index out of range"

-- | Substitute a value into the body of a function.
apply
    :: Exp a       -- ^ Argument value
    -> Exp (Inc a) -- ^ Function body
    -> Exp a       -- ^ Result
apply e = joinE . mapE (subst e . mapI Ref)

-- | Shift an 'Inc' down by one. If it falls below zero, replace it with
-- the value.
--
-- This is the inverse of 'match': for any value of @x@,
-- @subst x . match x === id@.
subst
    :: a     -- ^ Replacement value
    -> Inc a -- ^ Value to shift
    -> a     -- ^ Result
subst x O = x
subst _ (S y) = y

-- | Remove one layer of 'Inc', incrementing the index inside.
--
-- In "Sylvia.Text.PrettyPrint", this is used to get the original
-- indices from the structure.
--
-- This is the inverse of 'shiftUp'.
shiftDown
    :: Integral a
    => Inc a
    -> a
shiftDown O = 0
shiftDown (S index) = index + 1

-- | Evaluate an expression to head normal form.
evalHNF :: Exp a -> Exp a
evalHNF e = case e of
    App a b -> let
        -- Recurse in both branches
        a' = evalHNF a
        b' = evalHNF b
      in
        case a' of
            -- If we have a lambda on the left side, apply it
            Lam e' -> evalHNF $ apply b e'
            -- Otherwise, leave them as-is
            _      -> App a' b'
    _ -> e