Haskell 如何将多个具有类约束的非标准转换器组合到一个堆栈中?
这将是一个很长的过程,因为我不确定我是否以正确的心态进行了讨论,所以我将在每一步尽可能清晰地概述我的想法。我有两个代码片段,它们尽可能少,所以请随意使用 我从一个转换器FitStateT m a开始,它只保存程序当时的状态,并允许保存到磁盘:Haskell 如何将多个具有类约束的非标准转换器组合到一个堆栈中?,haskell,monad-transformers,Haskell,Monad Transformers,这将是一个很长的过程,因为我不确定我是否以正确的心态进行了讨论,所以我将在每一步尽可能清晰地概述我的想法。我有两个代码片段,它们尽可能少,所以请随意使用 我从一个转换器FitStateT m a开始,它只保存程序当时的状态,并允许保存到磁盘: data FitState = FitState newtype FitStateT m a = FitStateT (StateT FitState m a) deriving (Monad, MonadTrans) 在项目的某个阶段,我决定将hask
data FitState = FitState
newtype FitStateT m a = FitStateT (StateT FitState m a) deriving (Monad, MonadTrans)
在项目的某个阶段,我决定将haskeline添加到项目中,该项目具有如下数据类型:
-- Stuff from haskeline. MonadException is something that haskeline requires for whatever reason.
class MonadIO m => MonadException m
newtype InputT m a = InputT (m a) deriving (Monad, MonadIO)
因此,我的主文件中的例程如下所示:
myMainRoutineFunc :: (MonadException m, MonadIO m) => FitStateT (InputT m) ()
myMainRoutineFunc = do
myFitStateFunc
lift $ myInputFunc
return ()
myMainRoutineFunc :: (MonadInput t m, MonadFitState t m) => t m ()
myMainRoutineFunc = do
myFitStateFunc
myInputFunc
return ()
newtype App a = App { getApp :: StateT FitState (InputT IO) a }
不幸的是,随着我的程序的发展,这方面出现了许多问题。主要的问题是,对于我运行的每一个输入函数,我都必须在运行它之前提升它。另一个问题是,对于每个运行输入命令的函数,我都需要一个MonadException m约束。此外,对于运行fitstate相关函数的任何函数,它都需要MonadIO m约束
代码如下:
所以我决定创建一些类,使其更好地结合在一起,并稍微清理类型。我的目标是能够写出这样的东西:
myMainRoutineFunc :: (MonadException m, MonadIO m) => FitStateT (InputT m) ()
myMainRoutineFunc = do
myFitStateFunc
lift $ myInputFunc
return ()
myMainRoutineFunc :: (MonadInput t m, MonadFitState t m) => t m ()
myMainRoutineFunc = do
myFitStateFunc
myInputFunc
return ()
newtype App a = App { getApp :: StateT FitState (InputT IO) a }
首先,我创建了一个MonadInput类来包装输入类型,然后我自己的例程将成为这个类的一个实例
-- Stuff from haskeline. MonadException is something that haskeline requires for whatever reason.
class MonadIO m => MonadException m
newtype InputT m a = InputT (m a) deriving (Monad, MonadIO)
-- So I add a new class MonadInput
class MonadException m => MonadInput t m where
liftInput :: InputT m a -> t m a
instance MonadException m => MonadInput InputT m where
liftInput = id
我添加了MonadException约束,这样就不必在每个与输入相关的函数上单独指定它。这就需要添加MultiparamTypeClass和flexibleinstances,但生成的代码正是我想要的:
myInputFunc :: MonadInput t m => t m (Maybe String)
myInputFunc = liftInput $ undefined
于是我也为FitState做了同样的事情。我再次添加了MonadIO约束:
-- Stuff from my own transformer. This requires that m be MonadIO because it needs to store state to disk
data FitState = FitState
newtype FitStateT m a = FitStateT (StateT FitState m a) deriving (Monad, MonadTrans, MonadIO)
class MonadIO m => MonadFitState t m where
liftFitState :: FitStateT m a -> t m a
instance MonadIO m => MonadFitState FitStateT m where
liftFitState = id
这同样非常有效
myFitStateFunc :: MonadFitState t m => t m ()
myFitStateFunc = liftFitState $ undefined
然后我将我的主例程包装到一个新类型包装器中,这样我就可以创建这两个类的实例:
newtype Routine m a = Routine (FitStateT (InputT m) a)
deriving (Monad, MonadIO)
然后是MonadInput的一个实例:
instance MonadException m => MonadInput Routine m where
liftInput = Routine . lift
工作完美。现在,对于MonadFitState:
instance MonadIO m => MonadFitState Routine m where
liftFitState = undefined
-- liftFitState = Routine -- This fails with an error.
啊,糟糕,它失败了
Couldn't match type `m' with `InputT m'
`m' is a rigid type variable bound by
the instance declaration at Stack2.hs:43:18
Expected type: FitStateT m a -> Routine m a
Actual type: FitStateT (InputT m) a -> Routine m a
In the expression: Routine
In an equation for `liftFitState': liftFitState = Routine
我不知道该怎么做才能让这一切顺利。我真的不明白这个错误。这是否意味着我必须让FitStateT成为MonadInput的一个实例?这看起来很奇怪,这是两个完全不同的模块,没有任何共同之处。任何帮助都将不胜感激。有没有更好的方法来得到我想要的
已完成的代码有错误:首先,这里是
liftFitState的类型:
liftFitState :: MonadFitState t m => FitStateT m a -> t m a
下面是例程的类型:
Routine :: FitStateT (InputT m) a -> Routine m a
您的liftFitState
函数要求从FitStateT
转换单个包装类型,但例程有两层它包装的转换器。所以类型不匹配
除此之外,我真的怀疑你走错了方向
首先,如果你正在编写一个应用程序,而不是一个库,那么更常见的做法是将你需要的所有monad转换器封装在一个大堆栈中,并在任何地方使用。通常,将其作为转换器的唯一原因是在有限数量的基单子之间切换,例如标识
,IO
,ST
,或STM
。但是,如果您需要变压器堆栈的所有东西都需要IO,并且您不打算使用ST
或STM
,那么即使这样也太过分了
在您的案例中,最简单的方法显然如下所示:
myMainRoutineFunc :: (MonadException m, MonadIO m) => FitStateT (InputT m) ()
myMainRoutineFunc = do
myFitStateFunc
lift $ myInputFunc
return ()
myMainRoutineFunc :: (MonadInput t m, MonadFitState t m) => t m ()
myMainRoutineFunc = do
myFitStateFunc
myInputFunc
return ()
newtype App a = App { getApp :: StateT FitState (InputT IO) a }
…然后派生或手动实现所需的MonadFoo
类(例如MonadIO
),只需到处使用该堆栈即可
这样做的好处是,以后如果您需要像添加Haskeline那样添加另一个转换器,而不是在多个层上乱搞,那么决定为某种全局数据资源添加一个ReaderT
,比方说——您可以简单地将它添加到包装的堆栈中,而当前使用该堆栈的所有代码甚至都不知道它们之间的区别
另一方面,如果您真的想采用当前的方法,那么monad transformer提升习惯用法就有点错误了。基本的提升操作应该来自您已经推导的MonadTrans
MonadFoo
类通常用于为每个monad提供基本操作,例如get
和put
用于MonadState
您似乎在尝试模仿liftIO
,这是一种“一路提升”操作,以lift
足够的时间从堆栈底部--IO
--到达实际的monad。这对于可以出现在堆栈中任何位置的变压器来说都没有意义
如果您确实想拥有自己的MonadFoo
类,我建议查看MonadState
类的源代码,看看它们是如何工作的,然后遵循相同的模式。虽然这不是一个库,但我的问题是这个“例程”实际上是一个举重例程。我有大约5种不同的习惯,我想试试。现在前端是一个控制台程序,但我没有理由不在某个时候制作一个web前端。所以我有一个强烈的愿望,就是尽可能地将所有的内部构件分开,这样我就可以混合和匹配它们。我在liftIO之后做了这个模型,因为我不知道还有什么其他方法可以做。MonadState/Reader类似乎也有同样的问题?@mindreader IIRC,对于MonadState
,您可以通过为堆栈中最外层的转换器定义一个实例(或派生它)来解决这个问题,然后将get、put等操作提升到适当的级别。