我试图在Haskell中创建基于代理的系统。为此,我需要在逻辑上将代理和环境部分分开,例如使用不同的测试和真实环境运行。
组件类型,代理和环境都会有很多有状态的东西,所以我选择使用monad变换器堆栈来构建每个组件。我将组件接口移动到类型类,该类由环境实现。实现此类型类的Monad可用于通过将它们插入代理变换器来完成整个变换器堆栈。
我创建了一个工作概念证明,发布在下面。
{-# LANGUAGE MultiParamTypeClasses, FunctionalDependencies, FlexibleContexts,
GeneralizedNewtypeDeriving #-}
module MonadComponents where
import Control.Monad
import Control.Monad.Reader
import Control.Monad.Trans
class Monad m => Environment m v | m -> v where
envAction :: v -> m v
newtype AgentT r v m a = AgentT {unAgentT :: ReaderT r m a}
deriving (Monad, MonadTrans, MonadReader r)
runAgentT :: (Environment m v) => AgentT r v m a -> r -> m a
runAgentT = runReaderT . unAgentT
instance Environment IO Int where
envAction x = do
putStrLn $ "action performed in IO environment " ++ (show x)
return $ x * 2
agentAction :: (Environment m Int) => AgentT Int Int m Int
agentAction = do
x <- ask
lift $ envAction (x+10)
ioAction :: Int -> IO Int
ioAction = runAgentT agentAction
虽然这种方法很有效,但我发现代码存在两个问题。首先,AgentT本身不能说它只接受作为环境实例的Monads。这只是由runAgentT的类型签名强制执行。其次,变压器堆栈越深,完全评估后的结果类型就越复杂。例如,当它们在最后一个点的堆栈中时,你需要收集和处理StateT,WriterT,MaybeT,EitherT的每个结果。因此,在结果程序中有一个位置,不再存在组件分离。
我敢打赌,有很多不同的方法可以做到这一点。那么,你能想到通过Haskell中定义良好的接口来分离副作用组件的其他方法吗?