VTY-UI 需要 IO。我能让这件事发生吗?

VTY-UI needs IO. Can I make this happen?

我正在尝试使用 VTY-UI 库构建 UI。 我也在使用自定义 monad(几个 monad 堆叠在一起)。 对于常规 IO 函数,这不是问题。我可以将它们提升到我的 monad 中。但是,VTY-UI 函数 onActivate 具有此类型签名:

onActivate :: Widget Edit -> (Widget Edit -> IO ()) -> IO ()

有没有一种方法可以将 Widget Edit -> MyMonad () 函数转换为 (Widget Edit -> IO ()) 而无需包装和解包我的 monad? 我宁愿不将所有库的类型签名重写为 MonadIO m => m () 而不是 IO ().

函数 liftBaseOpDiscard from monad-control 似乎可以解决问题:

import Control.Monad.Trans.Control 

type MyMonad a = ReaderT Int (StateT Int IO) a

onActivate' :: Widget Edit -> (Widget Edit -> MyMonad ()) -> MyMonad ()
onActivate' = liftBaseOpDiscard . onActivate 

这个函数有一个 MonadBaseControl 约束,但是 ReaderTStateT 上面 IO 已经有那个类型类的实例。

正如 liftBaseOpDiscard 的文档中提到的,回调中对状态的更改将被丢弃。

MonadBaseControl 允许您暂时将 monad 堆栈的上层隐藏到堆栈的基本 monad 的值中 (liftBaseWith) and afterwards pop them again, if needed (restoreM)。

编辑: 如果我们需要保留回调中发生的效果(比如状态的变化),一种解决方案是使用 "mimic" 状态 IORef作为ReaderT的环境。写入 IORef 的值不会被丢弃。 monad-unlift 包就是围绕这个想法构建的。一个例子:

import Control.Monad.Trans.Unlift 
import Control.Monad.Trans.RWS.Ref
import Data.IORef

-- use IORefs for the environment and the state
type MyMonad a = RWSRefT IORef IORef Int () Int IO a

onActivate' :: Widget Edit -> (Widget Edit -> MyMonad ()) -> MyMonad ()
onActivate' we f = do 
    -- the run function will unlift into IO
    UnliftBase run <- askUnliftBase
    -- There's no need to manually "restore" the stack using
    -- restoreM, because the changes go through the IORefs
    liftBase $ onActivate we (run . f)

monad 之后可以 运行 使用 runRWSIORefT

状态部分:你可以使用这个模块。感谢那些意识到使 getput 多态化是个好主意的人。

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE UndecidableInstances #-}
module IState where
import Control.Monad
import Control.Monad.State
import Control.Monad.Reader
import Control.Monad.Trans.Class
import Control.Applicative
import Data.IORef

newtype IState s m a = IState (ReaderT (IORef s) m a)

runIState (IState a) s = do
  sr <- liftIO $ newIORef s
  runReaderT a sr

runIStateRef (IState a) r = runReaderT a r

instance (Monad m) => Monad (IState s m) where
  return = IState . return
  (IState a) >>= f = let
    f' i = let (IState i') = f i in i'
    in IState (a >>= f')

instance (Monad m,Functor m) => Applicative (IState s m) where
  pure = return
  (<*>) = ap

instance (Functor m) => Functor (IState s m) where
  fmap f (IState a) = IState (fmap f a)

instance (MonadIO m) => MonadIO (IState s m) where
  liftIO = lift . liftIO

instance (MonadState s' m) => MonadState s' (IState s m)   where
  get = lift get
  put = lift . put

-- Because of this instance IState is almost a drop-in replacement for StateT
instance (MonadIO m) => MonadState s (IState s m) where
  get = IState $ do
    r <- ask
    liftIO $ readIORef r
  put v = IState $ do
    r <- ask
    liftIO $ writeIORef r v

instance MonadTrans (IState s) where
  lift a = IState (lift a)

我成功实现了问题评论中提到的建议。

我在 IO 中提供 vty 回调,将事件发送到 Chan。然后我有另一个线程监听这些事件并在我自己的 monad 中执行适当的操作。