使用来自 REPL 的 Monadic eDSL
Using a Monadic eDSL from the REPL
假设我使用 monad 在 Haskell 中为自己创建了一种嵌入式领域特定语言。例如,一种简单的语言,可让您在堆栈上压入和弹出值,使用状态 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(特别是 GHCi,如果有帮助的话愿意使用其他的)以交互方式使用我的 DSL。
不幸的是session喜欢:
>push 10
>pop
10
>push 100
不会立即起作用,这可能是相当合理的。然而,我真的认为能够以类似的感觉做一些事情会很酷。 state monad 的工作方式并不适合这种情况。您需要构建您的 DSL a
类型,然后对其进行评估。
有没有办法做这样的事情。在 REPL 中增量使用 monad?
我一直在看 operational, MonadPrompt, and MonadCont 之类的东西,我觉得也许可以用来做这样的事情。不幸的是 none 我看到的示例解决了这个特定问题。
在某种程度上。
我不认为它可以用于任意 Monads/instruction 集,但这里有一些对您的示例有用的东西。我正在使用 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
将此技术用于 State/Reader/Writer/IO-based DSL 应该很简单。不过我不希望它适用于所有情况。
另一种可能性是每次你做任何事情时都重新模拟整个历史。这适用于任何纯 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
和一个让你脱离 monad 的 "run" 函数。 运行 函数具有类型 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
。后者,我认为,反正更惯用。
假设我使用 monad 在 Haskell 中为自己创建了一种嵌入式领域特定语言。例如,一种简单的语言,可让您在堆栈上压入和弹出值,使用状态 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(特别是 GHCi,如果有帮助的话愿意使用其他的)以交互方式使用我的 DSL。
不幸的是session喜欢:
>push 10
>pop
10
>push 100
不会立即起作用,这可能是相当合理的。然而,我真的认为能够以类似的感觉做一些事情会很酷。 state monad 的工作方式并不适合这种情况。您需要构建您的 DSL a
类型,然后对其进行评估。
有没有办法做这样的事情。在 REPL 中增量使用 monad?
我一直在看 operational, MonadPrompt, and MonadCont 之类的东西,我觉得也许可以用来做这样的事情。不幸的是 none 我看到的示例解决了这个特定问题。
在某种程度上。
我不认为它可以用于任意 Monads/instruction 集,但这里有一些对您的示例有用的东西。我正在使用 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
将此技术用于 State/Reader/Writer/IO-based DSL 应该很简单。不过我不希望它适用于所有情况。
另一种可能性是每次你做任何事情时都重新模拟整个历史。这适用于任何纯 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
和一个让你脱离 monad 的 "run" 函数。 运行 函数具有类型 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
。后者,我认为,反正更惯用。