4

这将是一个漫长的过程,因为我不确定我是否以正确的心态进行了讨论,所以我将在每一步中尽可能清楚地概述我的想法。我有两个尽可能少的代码片段,所以请随意使用它们。

我从单个转换器 FitStateT ma 开始,它只保存当时程序的状态并允许保存到磁盘:

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 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

4

1 回答 1

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 提供基本操作,例如getputfor MonadState

您似乎在尝试模仿liftIO,这是一个“一路提升”操作,lift足以从堆栈底部 - -IO到实际的 monad。对于可以出现在堆栈中任何位置的转换器,这实际上没有意义。

如果您确实想拥有自己的MonadFoo课程,我建议您查看类似课程的源代码MonadState并了解它们的工作方式,然后遵循相同的模式。

于 2012-12-23T19:03:17.847 回答