使用REPL中的Monadic eDSL

时间:2017-05-04 18:33:59

标签: haskell monads read-eval-print-loop ghci

假设我使用monad在Haskell中创建了一个嵌入式域特定语言。例如,一种简单的语言,允许您在堆栈上推送和弹出值,使用状态monad实现:

type DSL a = State [Int] a

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

现在我可以使用do notation编写小堆栈操作程序:

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

但是,我真的想从REPL交互使用我的DSL(特别是GHCi,如果有帮助的话,愿意使用其他的。)

不幸的是有一个类似的会话:

>push 10
>pop
10
>push 100

不会立即起作用,这可能是相当合理的。但是我真的觉得能够做一些类似的感觉会很酷。国家monad工作的方式不容易适应这种情况。您需要构建DSL a类型,然后对其进行评估。

有没有办法做这样的事情。在REPL中使用monad增量?

我一直在研究像operationalMonadPromptMonadCont这样的东西,我觉得可能会用来做这样的事情。不幸的是,我见过的所有例子都没有解决这个特殊问题。

2 个答案:

答案 0 :(得分:5)

另一种可能性是每次做任何事情时重新模拟整个历史。这适用于任何纯monad。这是一个即兴的库:

{-# LANGUAGE RankNTypes #-}

import Data.IORef
import Data.Proxy

newtype REPL m f = REPL { run :: forall a. m a -> IO (f a) }

newREPL :: (Monad m) => Proxy m -> (forall a. m a -> f a) -> IO (REPL m f)
newREPL _ runM = do
    accum <- newIORef (return ())
    return $ REPL (\nextAction -> do
        actions <- readIORef accum
        writeIORef accum (actions >> nextAction >> return ())
        return (runM (actions >> nextAction)))

基本上,它将迄今为止运行的所有操作存储在IORef中,每次执行某些操作时,它都会添加到操作列表中并从顶部运行。

要创建一个repl,请使用newREPL,为monad传递Proxy,然后使用“run”函数将其从monad传递出去。运行函数具有类型m a -> f a而不是m a -> a的原因是您可以在输出中包含额外信息 - 例如,您可能也想查看当前状态,在这种情况下,您可以使用f之类的:

data StateOutput a = StateOutput a [Int]
    deriving (Show)

但我刚刚使用它Identity,没有什么特别的。

Proxy参数是这样的,当我们创建一个新的repl实例时,ghci的默认不会咬我们。

以下是您使用它的方式:

>>> repl <- newREPL (Proxy :: Proxy DSL) (\m -> Identity (evalState m []))
>>> run repl $ push 1
Identity ()
>>> run repl $ push 2
Identity ()
>>> run repl $ pop
Identity 2
>>> run repl $ pop
Identity 1

如果额外的Identity线条噪音困扰您,您可以使用自己的仿函数:

newtype LineOutput a = LineOutput a
instance (Show a) => Show (LineOutput a) where
    show (LineOutput x) = show x

我必须做一个小改动 - 我不得不改变

type DSL a = State [Int] a

type DSL = State [Int]

因为您不能使用未完全应用的类型同义词,例如我说Proxy :: DSL时。我认为,后者无论如何都更具惯用性。

答案 1 :(得分:3)

在某种程度上。

我不相信可以对任意Monad /指令集进行操作,但这里的内容适用于您的示例。我正在使用operational和IORef来支持REPL状态。

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)

然后你就可以使用它:

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

将此技术用于基于状态/读取器/写入器/ IO的DSL应该是直截了当的。我不希望它适用于所有事情。