这将是一个漫长的过程,因为我不确定我是否以正确的思维方式进入了这个领域,因此我将在每一步都尽可能清楚地概述我的思路。我有两个代码片段,尽可能少,我可以随意使用它们。
我开始使用单个变换器FitStateT m a,它只保存程序的状态并允许保存到磁盘:
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 ()
不幸的是,随着程序的增长,这有很多问题。主要的问题是,对于我运行的每个输入功能,我必须在运行之前抬起它。另一个问题是每个运行输入命令的函数,我需要一个MonadException m约束。对于运行fitstate相关函数的任何函数,它还需要MonadIO m约束。
以下是代码:https://gist.github.com/4364920
所以我决定创建一些类来使它更好地融合在一起并稍微清理类型。我的目标是能够写出这样的东西:
myMainRoutineFunc :: (MonadInput t m, MonadFitState t m) => t m ()
myMainRoutineFunc = do
myFitStateFunc
myInputFunc
return ()
首先,我创建了一个MonadInput类来包装InputT类型,然后我自己的例程将成为这个类的一个实例。
-- 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约束,这样我就不必在每个与输入相关的函数上单独指定它。这需要添加多个参数和灵活的实例,但结果代码正是我想要的:
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包装器中,以便我可以创建这两个类的实例:
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的一个实例?这看起来很奇怪,那是两个完全不同的模块,没有任何共同之处。任何帮助,将不胜感激。有没有更好的方法来获得我正在寻找的东西?
已完成的代码有错误:https://gist.github.com/4365046
答案 0 :(得分:3)
嗯,首先,这是liftFitState
的类型:
liftFitState :: MonadFitState t m => FitStateT m a -> t m a
这是Routine
的类型:
Routine :: FitStateT (InputT m) a -> Routine m a
您的liftFitState
函数需要从FitStateT
转换单个包装类型,但Routine
包含两层变换器。因此类型不会匹配。
除此之外,我真的怀疑你是以错误的方式解决这个问题。
首先,如果你正在编写一个应用程序而不是一个库,那么在一个大堆栈中简单地包装你需要的所有monad变换器并在任何地方使用它是更常见的。通常,将其留作变压器的唯一原因是用于在有限数量的基础单子之间切换,例如, Identity
,IO
,ST
或STM
。但是,如果您需要变压器堆栈的所有内容都需要IO而且您不打算使用ST
或STM
,那就太过分了。
在您的情况下,最简单的方法显然看起来像这样:
newtype App a = App { getApp :: StateT FitState (InputT IO) a }
...然后派生或手动实现您想要的MonadFoo
类(例如MonadIO
),并在任何地方使用该堆栈。
以这种方式实现这一目标的好处是,如果您需要以添加Haskeline的方式添加另一个转换器,而不是使用多个层进行混乱,那么决定为某种类型添加ReaderT
比如说全局数据资源 - 你只需将它添加到包装堆栈中,当前使用堆栈的所有代码都不会知道差异。
另一方面,如果你确实想要采用当前的方法,那么你就会让monad变压器提升成语有点不对劲。基本的提升操作应来自MonadTrans
,您已经得到了这个操作。 MonadFoo
类通常用于为每个monad提供基本操作,例如: get
的{{1}}和put
。
你似乎试图模仿MonadState
,这是一个“一路向上”的操作,liftIO
足以从堆栈底部获取lift
- - 实际的monad。对于可以出现在堆栈中任何位置的变换器来说,这没有任何意义。
如果你想拥有自己的IO
课程,我建议你查看MonadFoo
等课程的来源,看看它们是如何工作的,然后按照相同的模式。