这是一个设计问题。
我目前有一个“App”类,它是一个 monad 转换器堆栈,我想添加到该堆栈的是一个可变的键/值存储。
我希望能够轻松更改该商店的具体实现方式。最初我希望它只是内存中的Map
,但稍后我可能会将其更改为基于数据库的东西。
首先想到的就是添加 StateT
到我的 monad 变压器堆栈。这对于内存中的 Map
来说效果很好。 ,但我认为这不适用于数据库实现,因为我不想每次都获取/更新整个状态,而只想获取/更新一个键/值组合。
第二个想法是编写我自己的 monad 转换器。但是当我查看现有的变压器时,它们都具有带有所有其他 monad 变压器实例的类,因此它们可以与其他变压器一起工作。我认为添加我自己的 monad 变压器并允许它与所有其他变压器良好地交互将涉及编写 n
(或者实际上可能是 2 * n
实例),因此它可以穿透其他变压器,而其他变压器也可以穿透它。从技术上讲,我只需要使用我正在使用的变压器来完成此操作,但这似乎有点老套。关于 monad 转换器堆栈是否是一个好主意,这里可能还有另一个争论,但我不想在此时完全改变我的应用程序架构。
所以我的第三个想法是拥有一个 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
然后我开始实现接口(interface),如下所示:
instance LookupMapM key value (StateT (Map key value) m) where ...
instance (...) => LookupMapM key value IO where ...
这看起来很有希望,但第二个实例似乎有问题......就像,我只能有一个 IO
实现(想必不同的数据库应该能够有自己的实现)。
我想我可以使用包 tagged identity 解决上述问题,这样我就可以标记不同的实现。
我注意到,如果我的“ map ”变压器不在堆栈的顶部,我仍然需要抬起它,但这不是一个大问题,我可以只维护 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))
但我不确定这是否能解决任何问题,或者只是增加额外的复杂性而没有任何好处。
有什么想法吗?
最佳答案
您必须将IO
放在堆栈的底部。除此之外,您将需要一个带有数据库连接信息和通过它访问数据库的函数的ReaderT
。然后将您需要的其他内容放在上面。
有关此类事情的示例,请参阅 Render开罗图书馆的 monad。它存储渲染上下文而不是数据库连接,但概念是相同的。
关于haskell - 如何在 monad 转换器堆栈中对可变键/值映射进行建模?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/76125393/