【问题标题】:Using a Monadic eDSL from the REPL使用来自 REPL 的 Monadic eDSL
【发布时间】:2023-03-19 20:49:01
【问题描述】:

假设我使用 monad 在 Haskell 中为自己创建了一种嵌入式领域特定语言。例如,一种简单的语言,可以让您在堆栈上推送和弹出值,使用 state monad 实现:

type DSL a = State [Int] a

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

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

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

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

很遗憾有这样的会议:

>push 10
>pop
10
>push 100

不会立即起作用,这可能是相当合理的。但是,我真的认为能够以类似的感觉做一些事情会很酷。 state monad 的工作方式并不容易做到这一点。您需要构建您的 DSL a 类型,然后对其进行评估。

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

我一直在研究诸如operationalMonadPromptMonadCont 之类的东西,我觉得它们可能可以用来做这样的事情。不幸的是,我看到的例子都没有解决这个特殊问题。

【问题讨论】:

  • 为您的语言编写解析器并运行您自己的 REPL?
  • 你当然不能对任意单子这样做。如果有问题的 monad 是 data Proxy a = Proxy 怎么办?当有人在提示符下运行Proxy 计算时,你会打印什么?
  • @Zeta 这是我曾想过的,但它不再是一个嵌入式 DSL。有时这就是你想要的,我问了这个问题,看看是否可以使用 REPL 中的嵌入式 DSL。

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


【解决方案1】:

另一种可能性是每次你做任何事情时都重新模拟整个历史。这适用于任何纯单子。这是它的临时库:

{-# 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,将 Proxy 传递给 monad,并传递一个“运行”函数,让您脱离 monad。 run 函数的类型为m a -&gt; f a 而不是m a -&gt; a 的原因是您可以在输出中包含额外信息——例如,您可能还想查看当前状态,在这种情况下您可以使用@ 987654328@点赞:

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

但我刚刚将它与 Identity 一起使用,它并没有什么特别之处。

Proxy 参数是为了让 ghci 的默认设置不会在我们创建新的 repl 实例时对我们造成影响。

这是你如何使用它:

>>> 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。无论如何,我认为后者更惯用。

【讨论】:

    【解决方案2】:

    在一定程度上。

    我不相信它可以用于任意的 Monads/指令集,但这里有一些对你的例子有用的东西。我正在使用带有 IORef 的 operational 来支持 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 应该很简单。不过,我不希望它适用于所有事情。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-10-04
      • 1970-01-01
      • 1970-01-01
      • 2013-05-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多