Haskell中为自定义Monad Transformer实现liftIO风格lift函数的问题
问题根因
该设计存在根本性缺陷,核心问题出在MonadDB类型类中liftDB的签名设计:liftDB的签名forall m' a. Monad m' => DatabaseT m' a -> m a对m'做了全称量化,意味着调用方可以任意选择m'的类型,而实例实现要求m'必须和实例的底层m完全相等,无法满足对任意m'的适配要求,因此触发编译错误。
参考的MonadIO之所以能正常工作,是因为IO是具体的基础Monad而非Monad变换器,不存在多态的底层参数,liftIO :: IO a -> m a仅要求把固定的IO动作提升到上层栈,不存在类型匹配冲突。
可行的实现方案
有两种主流方案可以实现跨栈调用数据库操作的需求:
方案1:固定DatabaseT的底层Monad(简单快捷)
如果没有自定义底层Monad的需求,可以直接把DatabaseT的底层固定为IO,此时它和IO一样是具体的Monad,完全可以照搬MonadIO的设计:
-- 不再保留多态的m参数,底层固定为IO type DatabaseT = StateT DatabaseState (ExceptT DatabaseError IO) class Monad m => MonadDB m where liftDB :: DatabaseT a -> m a -- 基础实例直接用id instance MonadDB DatabaseT where liftDB = id -- 上层变换器的实例可以通过lift实现,比如StateT的实例: instance MonadDB m => MonadDB (StateT s m) where liftDB = lift . liftDB -- 其他ExceptT、ReaderT等变换器的实例同理,也可以用GHC扩展自动推导
方案2:MTL风格类型类设计(更灵活,符合Haskell生态惯例)
这是Haskell生态更推荐的做法,不需要单独定义liftDB,而是把所有数据库操作直接抽象到MonadDB类型类中:
class Monad m => MonadDB m where connectDB :: DBMode -> m () isConnected :: m Bool -- 其余所有数据库操作都在这里声明 -- 给DatabaseT写基础实现实例 instance MonadIO m => MonadDB (DatabaseT m) where connectDB = 原connectDB实现 isConnected = 原isConnected实现
后续在上层Monad栈(比如ReaderT AppConfig DatabaseT)使用时,只需要配合GeneralizedNewtypeDeriving或者DeriveAnyClass扩展就能自动派生MonadDB实例,直接调用connectDB、isConnected方法即可,不需要手动写任何lift操作,可适配任意层数的变换器栈。
内容的提问来源于stack exchange,提问作者vipeout
相关产品推荐
相关产品推荐

