【问题标题】:Writing a Monad Transformer, does it really need so many hardcoded instances写一个Monad Transformer,真的需要这么多硬编码的实例吗
【发布时间】:2016-02-20 18:45:39
【问题描述】:

我是一个长期使用 monad 转换器的用户,第一次写 monad 转换器……我觉得我做了一些不必要的事情。

我们正在开发一个包含多个 DB 表的项目,并且将集合硬编码到不同的 monad 堆栈变得笨拙,因此我们决定将其分解为不同的可插拔 monad 转换器,以便我们在函数类型级别进行选择, 像这样

doSomething::(HasUserTable m, HasProductTable m)=>Int->m String

(HasXTable 是类,XTableT 是具体的 monad 转换器)。这些单独的 monad 转换器可以以完全模块化的方式插入或移除,并且可以存储 DB 句柄、需要 ResourceT 等......

我的第一次尝试是围绕 ReaderT,它将用于保存 DB 句柄。很明显,这是行不通的,因为 ReaderT(和 StateT 等)如果不使用硬编码的“提升”链就无法堆叠,从而破坏了堆叠元素的可插拔模块化。

唯一的解决方案似乎是编写完全独立的 ReaderT monad 副本,每个副本都允许在较低级别访问其他副本。这行得通,但解决方案充满了样板代码,像这样

class HasUserTable m where
    getUser::String->m User

newtype UserTableT m r = UserTableT{runUserTableT::String->m r}

--Standard monad instance stuff, biolerplate copy of ReaderT
instance Functor m=>Functor (UserTableT m) where....
instance Applicative m=>Applicative (UserTableT m) where....
instance Monad m=>Monad (UserTableT m) where....
instance Monad m=>HasUserTable (UserTableT m) where....

--Gotta hardcode passthrough rules to every other monad transformer
--in the world, mostly using "lift"....
instance MonadTrans BlockCacheT where....
instance (HasUserTable m, Monad m)=>HasUserTable (StateT a m)....
instance (HasUserTable m, Monad m)=>HasUserTable (ResourceT m)....
.... etc for all other monad transformers

--Similarly, need to hardcode passthrough rules for all other monads
--through the newly created one
instance MonadResource m=>MonadResource (UserTableT m) where....
instance MonadState a m=>MonadState a (UserTableT m) where....
instance (MonadBaseControl IO m) => MonadBaseControl IO (UserTableT m)....
.... etc for all other monad transformers

更糟糕的是,我们需要为我们添加的每个新的 monad 转换器添加更多的传递规则(即,我们添加的每个新表都需要传递所有其他表 monad 转换器,所以我们需要 n^2 个实例声明!)

有没有更简洁的方法来做到这一点?

【问题讨论】:

  • 这看起来很容易让人联想到可扩展的效果。 hackage.haskell.org/package/free-vl 有一个实现和对解释它的论文的引用。
  • “所以我们需要 n^2 个实例声明”这是 mtl 风格的 monad 转换器的一个众所周知的问题。如果你将你的类型写成ReaderT String m r,你可以使用广义的新类型派生来派生那些与读者相同的实例(这看起来像这里的大多数)。您可以将大多数实例替换为 MonadTrans t, HasUserTable m => HasUserTable (t m),但这种方式会扼杀类型推断,并且需要多个扩展。
  • @user2407038 使用通用MonadTrans t, HasUserTable m=>HasUserTable (t m) 的问题在于它也适用于 UserTableT,与我需要编写的正确实例相冲突。我怀疑这就是为什么存在 n^2 问题的原因(否则他们会为所有的单子变换器这样做)。我认为您对 n^2 问题的评论可能是我的问题的答案,虽然不是一个快乐的问题....对于单子转换器,甚至是 Haskell,您无法做得更好。如果您有参考讨论这个问题,我会接受它作为答案。

标签: haskell monads monad-transformers


【解决方案1】:

是的,这就是 monad 转换器的问题之一:当您添加一个新转换器时,您必须编写越来越多的样板实例。每次都是 n 个实例,总共 O(n^2) 个实例。例如,您可以观察到此缩放问题in the mtl source code。 Monad 转换器不易扩展。

现在,我们每天使用的大部分 monad 可以表示为 mtl 提供的转换器的某种组合,这意味着其他人已经完成了编写所有这些无聊实例的工作。但是这些转换器肯定不会涵盖每一个 monad,而且当你需要编写自己的monad 时,你会被咬。

这就是为什么一直在努力设计新方法来实现打字效果的原因。 Haskell 中的一个很好的例子是 Kiselyov 等人的 extensible-effects library,它采用基于 free monads 的代数方法来实现类型化。该库的设计在两篇文章中进行了描述:An Alternative to Monad Transformers,花一些时间描述mtl 方法的问题,以及More Extensible Effects,描述该库的更新和优化实现。

如果您想了解安全和可扩展效果类型的应用范围,请参阅 Edwin Brady 的 effects library 了解 Idris 语言。有很多资源可以解释effectstutorial、原始Programming and Reasoning with Algebraic Effects 文章和Resource-dependent Algebraic Effects 描述effects 的一些新功能。这个列表中可能还有一些我忘记的资源。

【讨论】:

  • 感谢您的澄清和指向可扩展效果的指针。我肯定会看看可扩展效果,尽管我现在认为这个问题已经解决,因为很多人已经指出 n^2 问题是真实的,并且是 Haskell 社区中正在进行的研究问题。 FWIW,在我看来这是一个非常重要的问题需要解决(除非 extensible-effect 已经解决了),这可能是我在 Haskell 中遇到的唯一大问题之一。
  • 嗯,一如既往,这是一个权衡。 Monad 转换器肯定比extensible-effects 更简单(而且更简单的是自己滚动),实际上,您为灵活性付出的代价实际上并没有那么高,正如我在回答中提到的那样。效果类型从一开始是可选的 - 大多数语言都不会像 Haskell 那样强加于你,而且在没有它的情况下,我们作为一个行业的 50 年一直保持着惊人的生产力!
猜你喜欢
  • 2016-01-18
  • 2020-11-29
  • 1970-01-01
  • 2011-03-30
  • 1970-01-01
  • 2013-12-26
  • 2011-07-01
相关资源
最近更新 更多