Semantics III: Exception (with State)

Suppose there is a language that supports state and catching exceptions. Let's try a program like this:

x := 10;
try {
  x := 20;
  raise myexception;
} catch myexception {
}
read x

What do we expect to be the final value of x?

It's going to be 20. When an exception or error happens, the current state must be preserved, not forgotten, because the we expect to see it later if we catch the exception.

This means we cannot use the model of the parser monad or the monad in the previous section:

MyState -> Either Exception (MyState, a)

This model says that the Left case for exceptions does not have the new state. This is OK for parser monads because a parser backtracks to an old state (input string) anyway; it is still OK in the previous section because I was not catching any exception, the whole interpreter aborts anyway.

But if exceptions are catchable, then you always have a new state no matter what. Only the “answer” part is absent when an exception happens. So we switch to this new model:

MyState -> (MyState, Either Exception a)

Stateful exception language set up

For simplicity and minimum distraction, this toy language just has a single global variable, call it x. It has just enough constructs to show my point.

data Expr = Num Integer
          | VarX                -- x
          | AssignX Expr        -- x := expr
          | Seq [Expr]          -- sequential execution
          | Raise Exception
          | CatcMyException Expr Expr -- try { expr } catch myexception { expr }

-- Possible exceptions.
data Exception = MyException | AnotherException
    deriving (Eq, Show)

-- The type of possible answers from the interpreter.
data Value = VN Integer
           | VNone
    deriving (Eq, Show)
in SemanticsException.hs

State and exception model

I have just one global variable, so I just use Value directly for my state. Here is my model:

data EM a = MkEM (Value -> (Value, Either Exception a))

unEM :: EM a -> Value -> (Value, Either Exception a)
unEM (MkEM f) = f
in SemanticsException.hs

Implementation of connectives and useful primitives: Note how in many places I carefully obtain the new state s1 and pass it on to the next thing to do.

pure :: a -> EM a
pure a = MkEM (\s -> (s, Right a))

(>>=) :: EM a -> (a -> EM b) -> EM b
MkEM f >>= k = MkEM (\s0 -> case f s0 of
                        (s1, Left e) -> (s1, Left e)
                        (s1, Right a) -> unEM (k a) s1)

getX :: EM Value
getX = MkEM (\s -> (s, Right s))

setX :: Value -> EM ()
setX i = MkEM (\_ -> (i, Right ()))

raise :: Exception -> EM a
raise e = MkEM (\s -> (s, Left e))

-- 1st param: run this first
-- 2nd param: handler if myexception happens
catchMyException :: EM a -> EM a -> EM a
catchMyException (MkEM f) handler = MkEM (\s0 -> case f s0 of
                                             (s1, Left MyException) -> unEM handler s1
                                             othercases -> othercases)
in SemanticsException.hs

With that, the interpreter is almost straightforward (and I don't have an environment because I don't have local variables or function application here):

mainInterp :: Expr -> Either Exception Value
mainInterp expr = snd (unEM (interp expr) VNone)
-- So I'm using VNone to initialize x.

interp :: Expr -> EM Value

interp (Num i) = pure (VN i)

interp VarX = getX

interp (AssignX e) =
    interp e
    >>= \val -> setX val
    >>= \_ -> pure VNone

interp (Seq es) = go es
  where
    -- The answer of the whole Seq is VNone if the list is empty, else the
    -- answer from the last expression.  But still run all expressions for the
    -- effects.
    go [] = pure VNone
    go [e] = interp e
    go (e:es) = interp e >>= \_ -> go es

interp (Raise e) = raise e

interp (CatcMyException expr handler) = catchMyException (interp expr) (interp handler)
in SemanticsException.hs

For CatcMyException, just be careful that it is not simply catchMyException expr handler.

The code file also includes the example program I used earlier:

example = Seq [ AssignX (Num 10)              -- x := 10;
              , CatcMyException               -- try {
                  (Seq [ AssignX (Num 20)     --   x := 20;
                       , Raise MyException    --   raise myexception;
                       ])                     -- } catch myexception
                  (Seq [])                    --   {}
              , VarX                          -- read x
              ]
-- What should you get the end? 10? 20?
-- Test with: mainInterp example
in SemanticsException.hs

Do test it out and see what happens.