写一个 Monad Transformer,真的需要那么多硬编码的实例吗
Writing a Monad Transformer, does it really need so many hardcoded instances
我是 monad transformer 的长期用户,也是 monad transformer 的第一次编写者....我觉得我做了一些不必要的事情。
我们正在开发一个有多个 DB table 的项目,并且将集合硬编码到不同的 monad 堆栈中变得很笨重,因此我们决定将其分解为不同的可插入 monad 转换器,以便我们选择然后在函数类型级别选择,像这样
doSomething::(HasUserTable m, HasProductTable m)=>Int->m String
(HasXTable 是 class,XTableT 是具体的 monad 转换器)。这些单独的 monad 转换器可以以完全模块化的方式插入或删除,并将存储 DB 句柄,需要 ResourceT 等....
我的第一次尝试是环绕 ReaderT,它将用于保存数据库句柄。很明显,这是行不通的,因为 ReaderT(和 StateT 等)如果不使用硬编码 "lift" 链就无法堆叠,从而破坏了堆栈元素的可插入模块化。
唯一的解决方案似乎是编写完全独立的 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 转换器添加更多的直通规则(即 - 我们添加的每个新 table 都需要直通所有其他 table monad 转换器,所以我们需要 n^2 个实例声明!)
有更简洁的方法吗?
是的,这是 monad 转换器的问题之一:当你添加一个新的转换器时,你必须编写越来越多的样板实例。每次都是 n 个实例,总共 O(n^2) 个实例。例如,您可以观察到这个缩放问题 in the mtl
source code。 Monad 转换器不易扩展。
现在,我们日常使用的 monad 中有很大一部分可以表示为 mtl
提供的转换器的某种组合,这意味着其他人已经完成了编写所有的工作那些无聊的事例。但是那些转换器肯定不会涵盖 每个 monad,而且每当您需要编写自己的 monad 时都会被咬到。
这就是为什么人们一直在努力设计新的打字效果方法。 Haskell 中的一个很好的例子是 Kiselyov 等人的 extensible-effects
library, which takes an algebraic approach to effect typing, based on free monads. The design of this library is described in two articles: An Alternative to Monad Transformers, which spends some time describing problems with the mtl
approach, and More Extensible Effects,描述了库的更新和优化实现。
如果您想了解安全和可扩展的效果类型可以走多远,请参阅 Edwin Brady 的 effects
library for the Idris language. There exist quite a lot of resources explaining effects
: a tutorial, the original Programming and Reasoning with Algebraic Effects article, and Resource-dependent Algebraic Effects 描述 effects
的一些新功能。可能还有一些我忘记在此列表中的资源。
我是 monad transformer 的长期用户,也是 monad transformer 的第一次编写者....我觉得我做了一些不必要的事情。
我们正在开发一个有多个 DB table 的项目,并且将集合硬编码到不同的 monad 堆栈中变得很笨重,因此我们决定将其分解为不同的可插入 monad 转换器,以便我们选择然后在函数类型级别选择,像这样
doSomething::(HasUserTable m, HasProductTable m)=>Int->m String
(HasXTable 是 class,XTableT 是具体的 monad 转换器)。这些单独的 monad 转换器可以以完全模块化的方式插入或删除,并将存储 DB 句柄,需要 ResourceT 等....
我的第一次尝试是环绕 ReaderT,它将用于保存数据库句柄。很明显,这是行不通的,因为 ReaderT(和 StateT 等)如果不使用硬编码 "lift" 链就无法堆叠,从而破坏了堆栈元素的可插入模块化。
唯一的解决方案似乎是编写完全独立的 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 转换器添加更多的直通规则(即 - 我们添加的每个新 table 都需要直通所有其他 table monad 转换器,所以我们需要 n^2 个实例声明!)
有更简洁的方法吗?
是的,这是 monad 转换器的问题之一:当你添加一个新的转换器时,你必须编写越来越多的样板实例。每次都是 n 个实例,总共 O(n^2) 个实例。例如,您可以观察到这个缩放问题 in the mtl
source code。 Monad 转换器不易扩展。
现在,我们日常使用的 monad 中有很大一部分可以表示为 mtl
提供的转换器的某种组合,这意味着其他人已经完成了编写所有的工作那些无聊的事例。但是那些转换器肯定不会涵盖 每个 monad,而且每当您需要编写自己的 monad 时都会被咬到。
这就是为什么人们一直在努力设计新的打字效果方法。 Haskell 中的一个很好的例子是 Kiselyov 等人的 extensible-effects
library, which takes an algebraic approach to effect typing, based on free monads. The design of this library is described in two articles: An Alternative to Monad Transformers, which spends some time describing problems with the mtl
approach, and More Extensible Effects,描述了库的更新和优化实现。
如果您想了解安全和可扩展的效果类型可以走多远,请参阅 Edwin Brady 的 effects
library for the Idris language. There exist quite a lot of resources explaining effects
: a tutorial, the original Programming and Reasoning with Algebraic Effects article, and Resource-dependent Algebraic Effects 描述 effects
的一些新功能。可能还有一些我忘记在此列表中的资源。