有点设计题
我目前有一个“App”类,它是一个 monad 转换器堆栈,我想添加到该堆栈的是一个可变的键/值存储。
我希望能够轻松更改该商店的确切实施方式。最初我希望它只是一个内存
Map
,但后来我可能会将它更改为一些基于数据库的东西。
首先想到的就是在我的 monad 转换器堆栈中添加一个
StateT
。这适用于内存中的Map
,但我认为这不适用于数据库实现,因为我不想每次都获取/更新entire状态,而只是一个键/值组合.
第二个想法是编写我自己的 monad 转换器。但是当我查看现有的转换器时,它们都有包含所有其他 monad 转换器实例的类,因此它们可以与其他转换器一起使用。我 认为 添加我自己的 monad 转换器并允许它与所有其他转换器很好地交互将涉及编写
n
(或者实际上可能是 2 * n
实例)以便它可以穿透其他转换器,其他转换器可以穿透它。从技术上讲,我只需要用我正在使用的变压器来做这件事,但这似乎有点老套。关于 monad 转换器堆栈是否是一个好主意,这里可能还有另一个争论,但我不想在这一点上完全改变我的 App 架构。
所以我的第三个想法是有一个
ReaderT
和我可以替换的函数记录(或者可能是带有类型类约束的记录,如 data Blah where Blah :: c => Blah
)。我认为我可以开始工作,但我不确定这是最好的方法,我必须在ReaderT
之上为我认为的内存方法分层StateT
。
作为我猜的第四种方法,我确实写了一堆这样的类型类:
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 解决上述问题,这样我就可以标记不同的实现。
我注意到如果我的“地图”转换器不在堆栈的顶部,我仍然需要提升它,但这不是什么大问题,我可以只维护一个
liftKV
功能,只需要一个额外的lift
每次将一层添加到 monad 转换器堆栈时,我认为这没什么大不了的。
但也许有比我建议的方法更好的方法?或者也许我建议的方法之一是最好的方法,我只是不确定是哪一种。
这里的其他想法不是:
class Monad m => LookupMapM key value m where
lookupM :: key -> m (Maybe value)
采用这种风格:
class Monad m => LookupMapM t m where
type Key t
type Value t
lookupM :: Key t -> m (Maybe (Value t))
但我不确定这是否解决了任何问题,或者只是增加了额外的并发症而没有任何好处。
有什么想法吗?