In a 'Type-Driven Development with Idris' book, there are couple of examples for encoding "programs" using a monadic datatype (leading to useful type-safe encodings with indexed datatypes). Usually, each such datatype can be run in a certain context (for learning purposes it's mostly IO
).
I attempted to write another 'runner', that would not be executed in a monadic context, but rather there would be a function to perform one step
given some input - if input matches the current program state, we'd feed its value and proceed, obtaining next program state.
This is simple to type if the datatype is not indexed:
data Prog : Type -> Type where
Req : Prog String
Pure : a -> Prog a
(>>=) : Prog a -> (a -> Prog b) -> Prog b
data Event = Resp String
run : Prog a -> Event -> Prog a
run Req (Resp s) = Pure s
run (Pure x) _ = Pure x
run (x >>= f) ev = let x' = run x ev
in case x' of
Pure v => f v
v => v >>= f
And a sample:
prog : Prog String
prog = do
x <- Req
y <- Req
Pure (x ++ y)
test : IO ()
test = do
-- this might doesn't look reasonable , but the point is that we could
-- keep/pass program state around and execute it in whatever manner we wish
-- in some environment
let s1 = run prog (Resp "Hello, ")
let s2 = run s1 (Resp "world")
case s2 of
Pure s => putStrLn s
_ => putStrLn "Something wrong"
This all seems to be working fine, but things get complicated when the main datatype is indexed, and tracks its state in dependently typed manner (depending on results):
data State = StateA | StateB
data Event = Resp String
data Indexed : (ty : Type) -> State -> (ty -> State) -> Type where
Req : Indexed String StateA (\res => case res of "frob" => StateB; _ => StateA)
Pure : (res : a) -> Indexed a (state_fn res) state_fn
(>>=) : Indexed a state1 state2_fn
-> ((res : a) -> Indexed b (state2_fn res) state3_fn)
-> Indexed b state1 state3_fn
Suddenly, it's not easy to type the run
function:
run : Indexed a s1 s2_fn -> Event -> Indexed a s3 s4_fn
won't cut it, because caller doesn't dictate the resulting state. This brought me to attempt at 'hiding' those parameters with dependent pair:
run : Indexed a s1 s2_fn -> Event -> (s3 ** s4_fn ** Indexed a s3 s4_fn)
meaning "run this program in particular state for me, I don't care what resulting state (indexes) will be".
But then, Pure
is getting problematic:
run (Pure x) _ = (?noIdea ** ?noIdeaAsWell ** (Pure x))
so maybe we also need input indexes:
run : (s1 ** s2_fn ** Indexed a s1 s2_fn) -> Event -> (s3 ** s4_fn ** Indexed a s3 s4_fn)
but the type errors soon became just too much, and lots of work would be just to 'recreate' the values for the dependent pair knowing the transition (which is already defined by transition anyway). This is leading me to thinking I'm on wrong track. How would one attempt at writing such interpreter?