2
votes

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?

1
Found a better way to express an existential type that would really mean "I don't care" (unlike with dependent pairs): gist.github.com/adituv/dcea611d75722560a3af64f5ae651804. Just need to find out how can it be applied to 'hide' more than one type variable.Bartosz
I'm just wondering if such a "step" way of interpreting computations makes sense at all, and maybe it just easier to implement a traditional "full run" function working in a context of some monad that would allow me to implement and satisfy whatever needs system it would be run within has (where to get inputs from, how to manage asynchrony and so on). Even if it would mean the interpreter there would have to allow for storing intermediate states and so on, it would work on simpler, non dependent-type...Bartosz

1 Answers

0
votes

I've pursued what I outlined in my second comment to the question. The type checker successfully convinced me there should be some different approach to that. If it's easy to write 'step' interpreter for a simpler datatype, and indexed datatypes make it harder, then why not define the run with some abstract datatype, that would allow us to build 'simply' typed structure easier for interpretation?

Let's recap:

data State = StateA | StateB

data Indexed : (ty : Type) -> State -> (ty -> State) -> Type where
  Req : Indexed String StateA (const 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

Now, let's define an execution context, and a run function that will operate in that context. The run will give us its final value, but will do this via some abstract EventCtx, because all we need is to grab the values from external events, and we don't care how the computation will have them handled:

namespace EventType
  data EventType = Req

data Event = Resp String

-- rename: eventType? what about EventType then :) ?
payloadType : EventType -> Type
payloadType EventType.Req = String

interface Monad m => EventCtx (m : Type -> Type) where
  fromEvent : (et : EventType) -> m (payloadType et)

run : EventCtx m => Indexed a s1 s2_fn -> m a
run Req = fromEvent EventType.Req
run (Pure res) = pure res
run (x >>= f) = do
  x' <- run x
  run (f x')

run is now just a standard affair, yay:)

OK, but let's check how we can still be able to run it step by step, with simpler type so that we can store intermediate states somewhere (while waiting for events to occur):

namespace SteppedRunner

  data Stepped : Type -> Type where
    Await : (et : EventType) -> Stepped (payloadType et)
    Pure : a -> Stepped a
    (>>=) : Stepped a -> (a -> Stepped b) -> Stepped b

  Functor Stepped where
    map f x = x >>= (\v => Pure (f v))

  Applicative Stepped where
    pure = Pure
    (<*>) f a = f >>= (\f' =>
                           a >>= (\a' =>
                                      Pure (f' a')))

  Monad Stepped where
    (>>=) x f = x >>= f

This is again fairly standard, what we have gained is a simpler type to make our interpretation easier, and free us from some heavy-duty types.

We also need an implementation of our abstract EventCtx, so that we'll be able to use run to turn our Indexed values to a Stepped one:

  EventCtx Stepped where
    fromEvent = Await

Now, our function to perform a step, given current state and an event:

  step : Stepped a -> Event -> Either (Stepped a) a
  step s e = fst (step' s (Just e))
    where
    step' : Stepped a -> Maybe Event -> (Either (Stepped a) a, Maybe Event)
    step' (Await Req) (Just (Resp s)) = (Right s, Nothing) -- event consumed
    step' s@(Await _) _ = (Left s, Nothing) -- unmatched event (consumed)
    step' (Pure x) e = (Right x, e)
    step' (x >>= f) e = let (res, e') = step' x e
                        in case res of
                             Left x' => (Left (x' >>= f), e')
                             Right x => step' (f x) e'

The main gist of it is that we can proceed only when we bind (>>=) a value, and we can obtain a value when we have Await and matching event. Rest is just to collapse the structure so that we're ready for another event value.

Some testing program:

test : Indexed String StateA (const StateA)
test = do
  x <- Req
  y <- do a <- Req
          b <- Req
          Pure (a ++ b)
  Pure (x++ y)

This is how we go from original, indexed datatype to stepped one:

s : Stepped String
s = run test

And now, just for a sake of obtaining a result in testing environment:

  steps : Stepped a -> List Event -> Either (Stepped a) a
  steps s evs = foldl go (Left s) evs
    where go (Right x) _ = Right x
          go (Left s) e = step s e

Some repling:

λΠ> steps s [Resp "hoho", Resp "hehe", Resp "hihihi"]
Right "hohohehehihihi" : Either (Stepped String) String