如何在单子转换器栈中建模可变键值映射?
单子转换器栈中添加可替换键值存储的设计方案探讨
我目前有一个作为单子转换器栈的App类,想要给这个栈添加可变键值存储功能,核心需求是能轻松切换存储实现:初期用内存中的Map,后续可能换成数据库实现。我自己梳理了几个方案,但拿不准最优解,想听听大家的看法:
方案一:在转换器栈中加入StateT
- 思路:用
StateT包裹内存Map,直接利用状态转换器的特性实现键值操作 - 优点:对内存
Map的支持非常顺畅,实现简单 - 缺点:完全不适用于数据库场景——数据库操作不需要每次获取/更新整个状态,只需要针对单个键值对做操作
方案二:自定义单子转换器
- 思路:自己编写一个专门处理键值存储的单子转换器
- 问题:要让自定义转换器和栈中其他现有转换器兼容,需要编写大量实例(大概n甚至2n个)来实现转换器间的穿透。理论上只针对当前在用的转换器写实例也能跑,但总感觉不够规范。另外我暂时不想彻底改动
App的整体架构。
方案三:使用带函数记录的ReaderT
- 思路:用
ReaderT持有包含键值操作的函数记录(或者带类型类约束的记录,比如data Blah where Blah :: c => Blah),通过读取这些函数来实现存储操作 - 疑问:这个方案可行,但不确定是不是最优解——比如内存实现可能需要在
StateT之上再叠加ReaderT,栈会变深
方案四:定义键值存储的类型类接口
先定义一套抽象操作的类型类:
data HadKey = KeyFound | KeyNotFound class Monad m => LookupMapM key value m where lookupM :: key -> m (Maybe value) class LookupMapM key value m => InsertMapM key value m where insertM :: key -> value -> m HadKey class InsertMapM key value m => UpdateMapM key value m where updateM :: key -> value -> m HadKey
然后给不同的单子实现这些接口,比如:
instance LookupMapM key value (StateT (Map key value) m) where ... instance (...) => LookupMapM key value IO where ...
- 优点:抽象程度高,切换实现只需要换对应的实例
- 问题:
IO只能有一个实例,不同的数据库实现没法区分。我想过用tagged identity给不同实现打标签来解决这个问题 - 额外小问题:如果键值相关的转换器不在栈顶,操作时需要
lift,但我可以维护一个liftKV函数,每次给栈加一层转换器就多套一个lift,这个问题不大
对方案四的一个改进想法
把原来的LookupMapM改成带关联类型的版本:
class Monad m => LookupMapM t m where type Key t type Value t lookupM :: Key t -> m (Maybe (Value t))
但不确定这样改是能解决问题,还是只是徒增复杂度。
想问问大家:有没有比这些更好的方案?或者其中某一个方案就是最优解,只是我没看出来?
内容的提问来源于stack exchange,提问作者Clinton
相关产品推荐
相关产品推荐

