rebound-0.1.0.0: examples/LC.hs
-- |
-- Module : LC
-- Description : Untyped lambda calculus
-- Stability : experimental
--
-- An implementation of the untyped lambda calculus including evaluation
-- and small-step reduction.
--
-- This module demonstrates the use of well-scoped lambda calculus terms using `Rebound`.
-- The natural number index `n` is the scoping level -- a bound on the number
-- of free variables that can appear in the term. If `n` is 0, then the
-- term must be closed.
module LC where
import Data.Fin
import Data.Vec qualified
import Rebound
import Rebound.Bind.Single
import Data.Fin
import Data.Vec qualified
import qualified Data.Maybe as Maybe
-- | Datatype of well-scoped lambda-calculus expressions
--
-- The @Var@ constructor of this datatype takes an index that must
-- be strictly less than the bound. Note that the type `Fin (S n)`
-- has `n` different elements.
-- The @Lam@ constructor binds a variable, using the the type `Bind`
-- from the library. The type arguments state that the binder is
-- for a single expression variable, inside an expression term, that may
-- have at most `n` free variables.
data Exp (n :: Nat) where
Var :: Fin n -> Exp n
Lam :: Bind Exp Exp n -> Exp n
App :: Exp n -> Exp n -> Exp n
deriving (Generic1)
----------------------------------------------
-- Example lambda-calculus expressions
----------------------------------------------
-- To make it easier to construct lambda calculus
-- expressions, we'll first define some helper
-- definitions
-- | a lambda expression
lam :: Exp (S n) -> Exp n
lam = Lam . bind
-- | an application expression
(@@) :: Exp n -> Exp n -> Exp n
(@@) = App
-- | variable with index 0
v0 :: Exp (S n)
v0 = Var f0
-- | variable with index 1
v1 :: Exp (S (S n))
v1 = Var f1
-- | The identity function "λ x. x".
-- With de Bruijn indices we write it as "λ. 0"
t0 :: Exp Z
t0 = lam v0
-- >>> t0
-- (λ. 0)
-- For example, we can write
-- (λx. ((x ((λy. y) x)) (λz. z)))
-- using this term with de Bruijn indices
-- (λ. ((0 ((λ. 0) 0)) (λ. 0)))
-- and then construct it with the definitions above
t :: Exp Z
t = lam ((v0 @@ ((lam v0) @@ v0)) @@ (lam v0))
----------------------------------------------
-- (Alpha-)Equivalence
----------------------------------------------
-- The nice thing about de Bruijn indices is that
-- we can use structural equality as alpha equivalence.
-- The built-in Eq instance for Bind, makes sure that
-- the delayed substitutions are not observable here.
deriving instance Eq (Exp n)
----------------------------------------------
-- Substitution
----------------------------------------------
-- To work with this library, we need two type class instances.
-- First, we tell the library how to construct variables in the expression
-- type. This class is necessary to construct an indentity
-- substitution---one that maps each variable to itself.
instance SubstVar Exp where
var :: Fin n -> Exp n
var = Var
-- Second, the operation `applyE` applies an environment
-- (explicit substitution) to an expression, and can be
-- automatically generated by the `Subst` type class, as
-- long as it can identify the variable constructor.
-- (Insead of generic programming, this operation can also
-- be written explicitly.)
instance Subst Exp Exp where
isVar (Var x) = Just (Refl, x)
isVar _ = Nothing
----------------------------------------------
-- Display (Show)
----------------------------------------------
-- | To show lambda terms, we use a simple recursive instance of
-- Haskell's `Show` type class. In the case of a binder, we use the `getBody`
-- operation to access the body of the lambda expression.
instance Show (Exp n) where
showsPrec :: Int -> Exp n -> String -> String
showsPrec _ (Var x) = shows x
showsPrec d (App e1 e2) =
showParen True $
showsPrec 10 e1
. showString " "
. showsPrec 11 e2
showsPrec d (Lam b) =
showParen True $
showString "λ. "
. shows (getBody b)
-----------------------------------------------
-- (big-step) evaluation
-----------------------------------------------
-- | Calculate the value of a lambda-calculus expression
-- This function looks like it uses call-by-value evaluation:
-- in an application it evaluates the argument `e2` before
-- using the `instantiate` function from the library to substitute
-- the bound variable of `Bind` by v. However, this is Haskell,
-- a lazy language, so that result won't be evaluated unless the
-- function actually uses its argument.
eval :: Exp Z -> Exp Z
eval (Var x) = case x of {}
eval (Lam b) = Lam b
eval (App e1 e2) =
let v = eval e2
in case eval e1 of
Lam b -> eval (instantiate b v)
t -> App t v
-- >>> t0
-- (λ. 0)
-- >>> eval (t `App` t0)
-- (λ. 0)
-- ((λ. (λ. 1)) ((λ. 0) (λ. 0)))
t2 = App (Lam (bind (Lam (bind (Var f1)))))
(App (Lam (bind (Var f0))) (Lam (bind (Var f0))))
-- >>> t2
-- ((λ. (λ. 1)) ((λ. 0) (λ. 0)))
-- >>> eval t2
-- (λ. (λ. 0))
----------------------------------------------
-- small-step evaluation
----------------------------------------------
-- | Do one step of evaluation, if possible
-- If the function is already a value or is stuck
-- this function returns `Nothing`
step :: Exp n -> Maybe (Exp n)
step (Var x) = Nothing
step (Lam b) = Nothing
step (App (Lam b) e2) = Just (instantiate b e2)
step (App e1 e2)
| Just e1' <- step e1 = Just (App e1' e2)
| Just e2' <- step e2 = Just (App e1 e2')
| otherwise = Nothing
-- | Evaluate the term as much as possible
eval' :: Int -> Exp n -> Maybe (Exp n)
eval' 0 e = Nothing
eval' k e = case step e of
Just e' -> eval' (k - 1) e'
Nothing -> Just e
-- >>> step (t0 `App` t0)
-- Just (λ. 0)
-- >>> eval' 5 (t `App` t0)
-- Just (λ. 0)
--------------------------------------------------------
-- full normalization
--------------------------------------------------------
-- | Calculate the normal form of a lambda expression. This
-- is like evaluation except that it also reduces underneath
-- the binders of @Lam@ expressions. There, we must first `getBody`
-- the binder and then rebind when finished
nf :: Exp n -> Exp n
nf (Var x) = Var x
nf (Lam b) = Lam (bind (nf (getBody b)))
nf (App e1 e2) =
case nf e1 of
Lam b -> nf (instantiate b e2)
t -> App t (nf e2)
--------------------------------------------------------
-- weak-head normalization / full reduction
--------------------------------------------------------
nf1 :: Exp n -> Exp n
nf1 (Var x) = Var x
nf1 (Lam b) = Lam (bind (nf1 (getBody b)))
nf1 (App e1 e2) =
case whnf e1 of
Lam b -> nf1 (instantiate b (whnf e2))
t -> App t (nf e2)
whnf :: Exp n -> Exp n
whnf (Var x) = Var x
whnf (Lam b) = Lam b
whnf (App e1 e2) =
case nf e1 of
Lam b -> nf (instantiate b (whnf e2))
t -> App t (nf e2)
--------------------------------------------------------
-- environment based evaluation / normalization
--------------------------------------------------------
-- invariant: expressions in the range of the environment are in whnf
whnfEnv :: Env Exp m n -> Exp m -> Exp n
whnfEnv r (Var x) = applyEnv r x
whnfEnv r (Lam b) = applyE r (Lam b)
whnfEnv r (App f a) =
case whnfEnv r f of
Lam b ->
instantiateWith b (whnfEnv r a) whnfEnv
-- unbindWith b (\r' e' -> whnfEnv (whnfrEnv r a .: r') e')
f' -> App f' (applyE r a)
-- >>> whnfEnv zeroE t -- start with "empty environment"
-- (λ. ((0 ((λ. 0) 0)) (λ. 0)))
-- For full reduction, we need to normalize under the binder too.
nfEnv :: Exp n -> Exp n
nfEnv (Var x) = Var x
nfEnv (Lam b) = Lam (bind (nfEnv (getBody b)))
nfEnv (App f a) =
case whnfEnv idE f of
Lam b -> nfEnv (instantiate b (whnfEnv idE a))
f' -> App (nfEnv f') (nfEnv a)
----------------------------------------------------------------