使用REPL的Monadic eDSL

And*_*iid 5 monads haskell ghci read-eval-print-loop

假设我使用monad在Haskell中创建了一种嵌入式领域特定语言。例如,使用状态monad实现的一种简单的语言就可以将值压入和弹出堆栈:

type DSL a = State [Int] a

push :: Int -> DSL ()
pop :: DSL Int
Run Code Online (Sandbox Code Playgroud)

现在,我可以使用do表示法编写小型堆栈处理程序:

program = do
    push 10
    push 20
    a <- pop
    push (5*a)
    return a
Run Code Online (Sandbox Code Playgroud)

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

不幸的是,像这样的会议:

>push 10
>pop
10
>push 100
Run Code Online (Sandbox Code Playgroud)

无法立即工作,这可能是合理的。但是我真的认为能够做些类似的事情会很酷。国家单子党的工作方式并不容易做到这一点。您需要建立DSL a类型,然后进行评估。

有没有办法做这样的事情。在REPL中逐渐使用monad吗?

我一直在寻找的东西一样操作MonadPromptMonadCont我排序得到的感觉可能的可以用来做这样的事情。不幸的是,我所看到的例子都没有解决这个特定问题。

luq*_*qui 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)))
Run Code Online (Sandbox Code Playgroud)

基本上,它会将到目前为止已运行的所有动作存储在中IORef,并且每次您执行某项操作时,它将添加到动作列表中并从顶部开始运行。

要创建一个repl,请使用newREPL,将其传递Proxy给monad并使用一个“运行”功能,使您脱离monad。run函数使用type m a -> f a而不是type的原因m a -> a是,这样您就可以在输出中包括额外的信息-例如,您可能还希望查看当前状态,在这种情况下,您可以使用flike:

data StateOutput a = StateOutput a [Int]
    deriving (Show)
Run Code Online (Sandbox Code Playgroud)

但是我只是使用它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
Run Code Online (Sandbox Code Playgroud)

如果多余的Identity线路噪音困扰您,您可以使用自己的函子:

newtype LineOutput a = LineOutput a
instance (Show a) => Show (LineOutput a) where
    show (LineOutput x) = show x
Run Code Online (Sandbox Code Playgroud)

我必须做一个小改变-我必须改变

type DSL a = State [Int] a
Run Code Online (Sandbox Code Playgroud)

type DSL = State [Int]
Run Code Online (Sandbox Code Playgroud)

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


Ste*_*out 3

在一定程度上。

我不相信它可以用于任意 Monads/指令集,但这里有一些适合您的示例的东西。我使用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)
Run Code Online (Sandbox Code Playgroud)

然后你可以像这样使用它:

> (push, pop) <- replSession [] -- this shadows the DSL push/pop definitions
> push 10
> pop
10
> push 100
Run Code Online (Sandbox Code Playgroud)

对于基于状态/读取器/写入器/IO 的 DSL 使用此技术应该很简单。但我并不指望它对所有事情都有效。