Search code examples
haskellmonadsread-eval-print-loopghci

Using a Monadic eDSL from the REPL


Say I have created myself an embedded domain specific language in Haskell using a monad. For example a simple language that lets you push and pop values on a stack, implemented using the state monad:

type DSL a = State [Int] a

push :: Int -> DSL ()
pop :: DSL Int

Now I can write small stack manipulation programs using do notation:

program = do
    push 10
    push 20
    a <- pop
    push (5*a)
    return a

However, I would really like to use my DSL interactively from a REPL (GHCi in particular, willing to use other if it would help).

Unfortunately having a session like:

>push 10
>pop
10
>push 100

Does not immediately work, which is probably rather reasonable. However I really think being able to do something with a similar feel to that would be cool. The way the state monad work does not lend itself easily to this. You need to build up your DSL a type and then evaluate it.

Is there a way to do something like this. Incrementally using a monad in the REPL?

I have been looking at things like operational, MonadPrompt, and MonadCont which I sort of get the feeling maybe could be used to do something like this. Unfortunately none of the examples I have seen addresses this particular problem.


Solution

  • To an extent.

    I don't believe it can be done for arbitrary Monads/instruction sets, but here's something that would work for your example. I'm using operational with an IORef to back the REPL state.

    data DSLInstruction a where
        Push :: Int -> DSLInstruction ()
        Pop :: DSLInstruction Int
    
    type DSL a = Program DSLInstruction a
    
    push :: Int -> DSL ()
    push n = singleton (Push n)
    
    pop :: DSL Int
    pop = singleton Pop
    
    -- runDslState :: DSL a -> State [Int] a
    -- runDslState = ...
    
    runDslIO :: IORef [Int] -> DSL a -> IO a
    runDslIO ref m = case view m of
        Return a -> return a
        Push n :>>= k -> do
            modifyIORef ref (n :)
            runDslIO ref (k ())
        Pop :>>= k -> do
            n <- atomicModifyIORef ref (\(n : ns) -> (ns, n))
            runDslIO ref (k n)
    
    replSession :: [Int] -> IO (Int -> IO (), IO Int)
    replSession initial = do
        ref <- newIORef initial
        let pushIO n = runDslIO ref (push n)
            popIO = runDslIO ref pop
        (pushIO, popIO)
    

    Then you can use it like:

    > (push, pop) <- replSession [] -- this shadows the DSL push/pop definitions
    > push 10
    > pop
    10
    > push 100
    

    It should be straightforward to use this technique for State/Reader/Writer/IO-based DSLs. I don't expect it to work for everything though.