如何将具有类约束的多个非标准变换器组合成一个堆栈?

时间:2012-12-23 18:30:03

标签: haskell monad-transformers

这将是一个漫长的过程,因为我不确定我是否以正确的思维方式进入了这个领域,因此我将在每一步都尽可能清楚地概述我的思路。我有两个代码片段,尽可能少,我可以随意使用它们。

我开始使用单个变换器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

1 个答案:

答案 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变换器并在任何地方使用它是更常见的。通常,将其留作变压器的唯一原因是用于在有限数量的基础单子之间切换,例如, IdentityIOSTSTM。但是,如果您需要变压器堆栈的所有内容都需要IO而且您不打算使用STSTM,那就太过分了。

在您的情况下,最简单的方法显然看起来像这样:

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等课程的来源,看看它们是如何工作的,然后按照相同的模式。