I'm working on a project that requires me to write a small interpreter. The instructions have a simple tree structure and one of the commands has the effect of halting execution. So in the example below, "baz" is never printed.
import Control.Monad.Cont
data Instruction = Print String | Halt | Block [Instruction]
deriving (Eq, Show)
instructions =
[ Print "foo"
, Block
[ Print "bar"
, Halt
]
, Print "baz"
]
main :: IO ()
main = runContT (callCC $ interpret instructions)
(const $ pure ())
interpret [] k = pure ()
interpret (a:as) k = case a of
Print str -> liftIO (putStrLn str) >> interpret as k
Block ins -> interpret ins k >> interpret as k
Halt -> k ()
This is the first time I've seen a potential use for ContT
in one of my projects. I was wondering if this is an appropriate use of it or if there is a simpler solution I might be overlooking.
Yes, this looks to be precisely the sort of use case for which Control.Monad.Cont
is appropriate.
You are almost certainly aware of this, but for other readers, it's worth spelling out that if you'd written interpret
so that it was a function from a list of instructions to an IO ()
like so:
main :: IO ()
main = interpret instructions
interpret :: [Instruction] -> IO ()
interpret [] = pure ()
interpret (a:as) = case a of
Print str -> putStrLn str >> interpret as
Block ins -> interpret ins >> interpret as
Halt -> pure ()
then foo
bar
and baz
all would have printed. What you needed was an escape mechanism that would allow you to abort the entire computation and immediately return a value. This is precisely what callCC
provides. Calling the named computation (k
in your code) allows you to escape out of the whole computation, not just that level/layer.
So good job, you've found precisely the appropriate use case for ContT here, I believe.