我已经用polysemy
库做了一些实验,并且很喜欢使用KVStore k v
,它是一个Key-Value-Store的简单抽象。现在我想知道我将如何定义一个类似的效果“mtl-style”。我对这个主题是新的,我还没有找到很多关于如何使用Monad转换器设计应用程序的信息。我也没有找到任何Monad来处理这种类型的效果。除了可能monad-persistent
,这似乎有点矫枉过正,为简单的问题,我试图解决。
我目前的方法是定义这个类型类:
class Monad m => KVStore k v m where
insert :: v -> k -> m ()
delete :: k -> m ()
lookup :: k -> m (Maybe v)
在这里,我已经遇到了delete
函数的问题,因为类型变量v
是不明确的。我的IDE建议我添加AllowAmbiguousTypes
,但我不明白这意味着什么。
接下来,我使用stm-containers
中的StmContainers.Map
实现了一个KVStore示例:
class HasSTMMap k v a where
stmMapL :: Lens' a (Map k v)
instance (Eq k, Hashable k, HasSTMMap k v r, MonadReader r STM) => KVStore k v STM where
insert v k = reader (view stmMapL) >>= Map.insert v k
delete k = reader (view stmMapL) >>= Map.delete k
lookup k = reader (view stmMapL) >>= Map.lookup k
删除函数同样会引起问题,因为它无法消除类型变量v
的歧义。
任何帮助都是感激不尽的,谢谢。
更新
多亏了K. A. Buhr的回答,我更新了我的项目结构:
class Monad m => MonadKVStore m k v where
insertKV :: v -> k -> m ()
deleteKV :: k -> m ()
lookupKV :: k -> m (Maybe v)
class HasSTMStore k v a where
stmStoreL :: Lens' a (Map k v)
type AppM env = ReaderT env Handler
instance HasSTMStore k v (Map k v) where
stmStoreL = id
instance (Eq k, Hashable k, HasSTMStore k v env) => MonadKVStore (AppM env) k v where
insertKV key value = Reader.asks (Lens.view stmStoreL)
>>= IO.liftIO . STM.atomically . Map.insert key value
deleteKV key = do
(store :: Map k v) <- Reader.asks (Lens.view stmStoreL)
IO.liftIO $ STM.atomically $ Map.delete key store
lookupKV key = Reader.asks (Lens.view stmStoreL)
>>= IO.liftIO . STM.atomically . Map.lookup key
出于测试目的,我使用了一个纯Map容器,如下所示:
type TestM k v = Reader (Map k v)
instance Ord k => MonadKVStore (TestM k v) k v where
...
1条答案
按热度按时间eqqqjvef1#
首先,类定义:
(我刚刚用GHC 7.10.3、8.10.7和9.0.2测试了它。)是这段代码本身给了你一个不明确的类型错误,还是其他什么原因?
总之,this answer解释了
AllowAmbiguousTypes
扩展。简而言之,GHC中有一个检查,防止定义(在大多数情况下)不能在vanilla Haskell中调用,因为它们的类型永远不能被解析。AllowAmbiguousTypes
扩展跳过了这个检查。结果函数 * 仍然 * 不能在vanilla Haskell中调用,但是它们通常可以通过另一个扩展来调用,比如TypeApplications
。因此,
AllowAmbiguousTypes
是无害的,您可以随意启用它,但需要注意的是,您最终可能需要使用TypeApplications
来应用它允许您定义的函数。但是,这并不是你关于如何定义mtl风格的
KVStoreT
monad转换器的问题的核心。当我实现一个单子转换器时,我通常从实现非转换器版本开始。(例如,
runKVStoreAsState
与runKVStorePure
),而后者通常坚持使用固定的实现。您应该从KVStore
单子的固定实现开始,也许可以使用一个类似状态的单子,其状态为Map
:请注意,这个单子非常类似于非转换器
State
单子:您可以在关于Haskell单子的旧参考文献中找到它和/或在tutorial中用作
State
的定义。现在,我觉得继续使用这个例子会破坏您的项目,所以让我来指导您开发一个不同的例子--一个带有
set
和count
操作的CounterT
。正如我所说,我通常从定义单子的非转换器版本开始:和其操作:
下面是一个快速测试:
在运行非转换器实现的情况下,我现在才将其转换为转换器
CounterT
:其关联示例:
和操作:
第一次从一个普通的单子到它的transformer版本的转换是很复杂的,在这里有一个非transformer版本的纯引用实现是一个很大的帮助。
请注意,这个转换器已经部分可用,即使我们没有定义相应的
transformer
和mtl
类:为了能够进行
lift
操作(例如,在CounterT IO
中使用IO
操作),我们需要一个MonadTrans
示例:我们还可以通过
MonadIO
示例定义liftIO
,以便通过大型堆栈将操作一直提升到基本IO单子,而不需要lift
链:现在我们可以写出这样的例子:
我们还应该定义一个简单的计数单子,它转换恒等单子(类似于现代的
State
是如何用StateT
定义的)加上它的runner:到目前为止,我们已经构建了一个
transformers
包风格的转换器。mtl
转换器的区别在于,你不需要lift
命名操作,比如count
和set
。为了支持这一点,我们需要将操作移到一个类中,这个类可以应用于任何带有CounterT
转换器的单子栈:并为
CounterT
转换器定义一个示例:现在我们来看看丑陋的样板文件。对于生态系统中的每一个其他转换器,我们需要定义一个
MonadCounter
示例来提升通过转换器的CounterT
操作。下面是IdentityT
和ReaderT
的示例:所有其他示例将具有基本相同的形式。
此外,对于生态系统中的几乎所有其他变压器,我们需要为
CounterT
定义适当的示例,以通过我们的变压器提升 * 它们 * 的操作。由于IdentityT
没有操作,因此不需要示例,但ReaderT
和其他变压器需要示例。下面是一个示例:型
现在,我们可以混合reader和counter操作,而不需要显式提升,不管我们的单子是如何堆叠的:
下面是完整的代码:
下面是
KVStore
的代码,它的形式几乎完全相同。请注意,对于此实现,我确实不得不使用AllowAmbiguousTypes
扩展,并发现我需要使用TypeApplications
来调用delete
函数。甚至insert
和lookup
也需要相当多的类型提示才能轻松调用。不过,我想您在使用多义版本的KVStore
时也会遇到同样的问题。扰流板
。
。
扰流板
。
。
扰流板
。
。
存储器