packages feed

bound-2.0.4: examples/Imperative.hs

{-# LANGUAGE DeriveFunctor, DeriveFoldable, DeriveTraversable, RankNTypes, ScopedTypeVariables #-}
module Main where

-- It's possible to use bound "sideways" in order to support terms which do not
-- have a Monad instance. A typical situation in which this would happen is when
-- modelling an imperative language: variables are bound by statements, but they
-- are used in positions where it would make no sense to replace them by another
-- statement.

import Bound.Scope -- .Simple
import Bound.Var
import Control.Monad (ap)
import Data.Functor.Identity
import Data.IORef
import Data.Void (Void, absurd)

-- PART 1: We want to model a tiny assembly language.
--
--   %0 = add 1 2
--   %1 = add %0 %0
--   ret %1
--
-- Add binds a fresh variable, and its operands can either be literals or
-- previously-bound variables. Ret must be the last instruction.
--
-- Operand is monadic, traversable, and satisfies all the other requirements in
-- order to be used with bound. But this is not sufficient, since Operand is
-- not the whole language: we also need to define Prog, the sequence of
-- instructions.
data Operand a
  = Lit Int
  | Var a
  deriving (Eq,Ord,Show,Read,Functor,Foldable,Traversable)

instance Applicative Operand where
  pure = Var
  (<*>) = ap

instance Monad Operand where
  return = pure
  Lit i >>= _ = Lit i
  Var x >>= f = f x

-- The following definition correctly models the instructions and their free
-- variables. But since the Var in Operand cannot be replaced with a Prog, this
-- definition is not monadic, and so we cannot manipulate the (Scope () Prog a)
-- using bound's functions. This defeats the point of using Scope at all!
--
--   data Prog a
--     = Ret (Operand a)
--     | Add (Operand a) (Operand a)
--           (Scope () Prog a) -- one more bound variable, available
--                             -- in the rest of the program
--
-- The sideways trick is to replace the Operand constructor with a (* -> *) type
-- parameter. Instantiating this with the real Operand will allow Operand to
-- access the same free variables as Prog. But if we instantiate this with
-- (Scope () Operand) instead, then the operands will have access to one extra
-- bound variable! This way, we can bind fresh variables which can only be used
-- inside the operands, and not in Prog.
data Prog operand a
  = Ret (operand a)
  | Add (operand a) (operand a)
        (Prog (Scope () operand) a)
  deriving (Eq,Ord,Show,Read,Functor,Foldable,Traversable)

-- The fact that the variables are not available in Prog after they are bound
-- might seem strange, and we'll fix this in part 2, but it is actually a good
-- thing. We want to be able to replace those variables with operand values, and
-- that would not be possible if variables were allowed to appear inside Prog
-- but outside of an operand.
pInstantiate1 :: forall operand b a. (Applicative operand, Monad operand)
              => operand a
              -> Prog (Scope b operand) a
              -> Prog operand a
pInstantiate1 = go instantiate1
  where
    -- A value of type (Prog (Scope b operand) a) contains operands of type
    -- (Scope b operand a), on which we can call instantiate1:
    --
    --   instantiate1 :: operand a -> Scope b operand a -> operand a
    --
    -- In the function below, (Scope b operand) and operand become o and o',
    -- and instantiate1 is called f:
    --
    --   f :: operand v -> o v -> o' v
    go :: forall o o' u. (Monad o, Monad o')
       => (forall v. operand v -> o v -> o' v)
       -> operand u -> Prog o u -> Prog o' u
    go f x (Ret o)        = Ret (f x o)
    go f x (Add o1 o2 cc) = Add (f x o1) (f x o2)
                          $ go f' x cc
      where
        -- The rest of the program has access to one extra variable:
        --
        --   cc :: Prog (Scope () (Scope b operand)) a
        --
        -- In there, the operands have type (Scope () (Scope b operand) a), and
        -- this time we cannot call instantiate1 because it would instantiate ()
        -- instead of instantiating b. Instead, we create a function f' which
        -- preserves the outer (Scope ()):
        --
        --   f' :: operand a -> Scope () (Scope b operand) a -> Scope () operand a
        --   f' :: operand a -> Scope () o                 a -> Scope () o'      a
        --
        -- In the recursive call to go, (Scope () (Scope b operand)) and
        -- (Scope () operand) become o and o', and f' is called f.
        f' :: operand v -> Scope () o v -> Scope () o' v
        f' v = toScope . f (fmap F v) . fromScope

pAbstract1 :: forall operand a. (Applicative operand, Monad operand, Eq a)
           => a
           -> Prog operand a
           -> Prog (Scope () operand) a
pAbstract1 = go abstract1
  where
    go :: forall o o' u. (Eq u, Monad o, Monad o')
       => (forall v. Eq v => v -> o v -> o' v)
       -> u -> Prog o u -> Prog o' u
    go f x (Ret o)        = Ret (f x o)
    go f x (Add o1 o2 cc) = Add (f x o1) (f x o2)
                          $ go f' x cc
      where
        f' :: forall v. Eq v => v -> Scope () o v -> Scope () o' v
        f' v = toScope . f (F v) . fromScope

evalOperand :: Operand Void -> Int
evalOperand (Lit i)    = i
evalOperand (Var void) = absurd void

-- |
-- >>> :{
-- let Just prog = closed
--               $ Add (Lit 1) (Lit 2)       $ pAbstract1 "%0"
--               $ Add (Var "%0") (Var "%0") $ pAbstract1 "%1"
--               $ Ret (Var "%1")
-- :}
--
-- >>> evalProg prog
-- 6
evalProg :: Prog Operand Void -> Int
evalProg (Ret o)        = evalOperand o
evalProg (Add o1 o2 cc) = evalProg cc'
  where
    result :: Int
    result = evalOperand o1 + evalOperand o2

    cc' :: Prog Operand Void
    cc' = pInstantiate1 (Lit result) cc


-- PART 2: Here's a slightly more complicated language.
--
--   %0 = add 1 2
--   %1 = add %0 %0
--   swp %0 %1
--   ret %1
--
-- The new swp command swaps the contents of two variables, so the two arguments
-- must be previously-bound variables, they cannot be literals. This time the
-- naïve definition looks like this:
--
--   data Prog' a
--     = Ret' (Operand a)
--     | Swp' a a
--            (Prog' a)
--     | Add' (Operand a) (Operand a)
--            (Scope () Prog' a)
--
-- If we apply the sideways trick to this definition, the newly-bound variables
-- will only be available in the operands, and so it won't be possible to call
-- swp on them. The first step towards a solution is to add seemingly-useless
-- Identity wrappers:
--
--   data Prog' a
--     = Ret' (Operand a)
--     | Swp' (Identity a) (Identity a)
--            (Prog' a)
--     | Add' (Operand a) (Operand a)
--            (Scope () Prog' a)
--
-- We can now apply the sideways trick twice: once for Operand, and once for
-- Identity. This gives us a lot of control: we can bind fresh variables which
-- can only be used inside the operands, we can bind fresh variables which can
-- be used inside Prog but not inside the operands, and as required for this
-- example, we can bind fresh variables which can be used in both.
data Prog' operand identity a
  = Ret' (operand a)
  | Swp' (identity a) (identity a)
         (Prog' operand identity a)
  | Add' (operand a) (operand a)
         (Prog' (Scope () operand) (Scope () identity) a)
  deriving (Eq,Ord,Show,Read,Functor,Foldable,Traversable)

-- Bound variables can now occur in both operand and identity, so we can no
-- longer instantiate them with operands. Instead, we'll have to instantiate
-- them with a value which both (Operand a) and (Identity a) can contain:
-- a free variable.
pInstantiate1' :: ( Applicative operand, Monad operand
                  , Applicative identity, Monad identity
                  )
               => a
               -> Prog' (Scope () operand) (Scope () identity) a
               -> Prog' operand identity a
pInstantiate1' = go (instantiate1 . pure) (instantiate1 . pure)
  where
    go :: forall o o' i i' u. (Monad i, Monad i', Monad o, Monad o')
       => (forall v. v -> o v -> o' v)
       -> (forall v. v -> i v -> i' v)
       -> u -> Prog' o i u -> Prog' o' i' u
    go fo fi x = go'
      where
        go' (Ret' o)        = Ret' (fo x o)
        go' (Swp' i1 i2 cc) = Swp' (fi x i1)
                                   (fi x i2)
                                   (go' cc)
        go' (Add' o1 o2 cc) = Add' (fo x o1)
                                   (fo x o2)
                                   (go fo' fi' x cc)

        fo' :: v -> Scope () o v -> Scope () o' v
        fo' v = toScope . fo (F v) . fromScope

        fi' :: v -> Scope () i v -> Scope () i' v
        fi' v = toScope . fi (F v) . fromScope

pAbstract1' :: ( Applicative operand, Monad operand
               , Applicative identity, Monad identity
               , Eq a
               )
            => a
            -> Prog' operand identity a
            -> Prog' (Scope () operand) (Scope () identity) a
pAbstract1' = go abstract1 abstract1
  where
    go :: forall o o' i i' u. (Eq u, Monad i, Monad i', Monad o, Monad o')
       => (forall v. Eq v => v -> o v -> o' v)
       -> (forall v. Eq v => v -> i v -> i' v)
       -> u -> Prog' o i u -> Prog' o' i' u
    go fo fi x = go'
      where
        go' (Ret' o)        = Ret' (fo x o)
        go' (Swp' i1 i2 cc) = Swp' (fi x i1)
                                   (fi x i2)
                                   (go' cc)
        go' (Add' o1 o2 cc) = Add' (fo x o1)
                                   (fo x o2)
                                   (go fo' fi' x cc)

        fo' :: Eq v => v -> Scope () o v -> Scope () o' v
        fo' v = toScope . fo (F v) . fromScope

        fi' :: Eq v => v -> Scope () i v -> Scope () i' v
        fi' v = toScope . fi (F v) . fromScope

evalOperand' :: Operand (IORef Int) -> IO Int
evalOperand' (Lit i)   = return i
evalOperand' (Var ref) = readIORef ref

-- |
-- >>> :{
-- let Just prog' = closed
--                $ Add' (Lit 1) (Lit 2)       $ pAbstract1' "%0"
--                $ Add' (Var "%0") (Var "%0") $ pAbstract1' "%1"
--                $ Swp' (Identity "%0") (Identity "%1")
--                $ Ret' (Var "%1")
-- :}
--
-- >>> evalProg' prog'
-- 3
evalProg' :: Prog' Operand Identity (IORef Int) -> IO Int
evalProg' (Ret' o)        = evalOperand' o
evalProg' (Swp' (Identity ref1) (Identity ref2) cc) = do
    x <- readIORef ref1
    y <- readIORef ref2
    writeIORef ref1 y
    writeIORef ref2 x
    evalProg' cc
evalProg' (Add' o1 o2 cc) = do
    result <- (+) <$> evalOperand' o1 <*> evalOperand' o2
    ref <- newIORef result
    evalProg' (pInstantiate1' ref cc)


main :: IO ()
main = return ()