我想在hedis(redis lib)之上编写一个简单的DSL。目标是编写如下函数:
iWantThis :: ByteString -> MyRedis ()
iWantThis bs = do
load bs -- :: MyRedis () It fetches a BS from Redis and puts it as
-- the state in a state monad
bs <- get -- :: MyRedis (Maybe ByteString) Gets the current state
put $ doSomethingPure bs -- :: MyMonad () Updates the state
commit -- :: MyRedis () Write to redis
基本思路是从redis中获取数据,将其置于状态monad中,对状态执行一些操作,然后将更新后的状态重新置于redis中。
显然,它应该是原子的,因此load
和put
应该在同一个Redis事务中发生。 Hedis允许通过将Redis
中的RedisTx (Queued a)
来电包裹起来。例如,我们有get :: ByteString -> RedisTx (Queued a)
。
Queued
是一个monad,然后您在multiExec
上运行Queued a
以执行同一事务中Queued a
中的所有内容。所以我试着定义我的MyRedis
:
import qualified Database.Redis as R
newtype MyRedis a = MyRedis { runMyRedis :: StateT MyState R.RedisTx a } -- deriving MonadState, MyState...
run
函数调用multiExec
所以我确信只要我留在MyRedis
,所有事情都会在同一个交易中发生。
run :: MyRedis (R.Queued a) -> MyState -> IO (R.TxResult a)
run m s = R.runRedis (undefined :: R.Connection) (R.multiExec r)
where r = evalStateT (runMyRedis m) s
此外,我可以将commit
定义为:
commit :: ByteString -> MyRedis (R.Queued R.Status)
commit bs = do
MyState new <- get
(MyRedis . lift) (R.set bs new)
computation
看起来像是:
computation :: MyRedis (R.Queued R.Status)
computation = do
load gid
MyState bs <- get
put $ MyState (reverse bs)
commit gid
where gid = "123"
但我无法弄清楚如何写“加载”
load :: ByteString -> MyRedis ()
load gid = undefined
实际上,我认为无法编写load
,因为get
的类型为ByteString -> RedisTx (Queued (Maybe ByteString))
,我无法查看Queued
monad执行它。
问题:
正确的是,由于Hedis获取的类型,用上面的语义定义load
函数是没有意义的吗?
是否可以更改MyRedis
类型定义以使其有效?
Hedis没有定义RedisT
monad变换器。如果存在这样的变压器,它会有任何帮助吗?
Hedis定义(但不导出到lib用户)MonadRedis
类型类;会让我的monad成为那个类型类帮助的一个实例吗?
这是正确的做法吗?我想:
MyRedis
get
和set
)您可以使用http://pastebin.com/MRqMCr9Q处的代码。抱歉,对于pastebin,lpaste.net目前正在关闭。
答案 0 :(得分:3)
你想要的是不可能的。特别是,在一个Redis事务中运行计算时,不能提供monadic接口。与您正在使用的库无关 - 这不是Redis可以做的事情。
Redis事务与您可能习惯使用的关系数据库世界的ACID事务有很大不同。 Redis事务具有批处理语义,这意味着以后的命令不能以任何方式依赖于先前命令的结果。
看:这里有类似你的例子,在Redis命令行上运行。
> set "foo" "bar"
OK
> multi
OK
> get "foo"
QUEUED -- I can't now set "baz" to the result of this command because there is no result!
> exec
1) "bar" -- I only get the result after running the whole tran
无论如何,这就是那个库有点奇怪Queued
类型的目的:这个想法是阻止你在批处理结束之前访问批处理命令的任何结果。 (似乎作者想要对批处理和非批处理命令进行抽象,但有更简单的方法可以做到这一点。请参阅下文,了解如何简化事务接口。)
因此,当涉及Redis事务时,没有“选择下一步做什么”,但(>>=) :: m a -> (a -> m b) -> m b
的重点是后来的效果可能取决于早期的结果。你必须在monad和交易之间做出选择。
如果您决定要进行交易,可以使用名为Monad
的{{1}}替代方法,它可以支持纯静态效果。这正是我们所需要的。这里有一些(完全未经测试的)代码,说明了我如何烹饪Applicative
版本的想法。
Applicative
如果我想抽象出事务或非行为,正如库作者试图做的那样,我会编写第二种类型newtype RedisBatch a = RedisBatch (R.RedisTx (R.Queued a))
-- being a transactional batch of commands to send to redis
instance Functor RedisBatch where
fmap = liftA
instance Applicative RedisBatch where
pure x = RedisBatch (pure (pure x))
(RedisBatch rf) <*> (RedisBatch rx) = RedisBatch $ (<*>) <$> rf <*> rx
-- no monad instance
get :: ByteString -> RedisBatch (Maybe ByteString)
get key = RedisBatch $ get key
set :: ByteString -> ByteString -> RedisBatch (R.Status)
set key val = RedisBatch $ set key val
runBatch :: R.Connection -> RedisBatch a -> IO (R.TxResult a)
runBatch conn (RedisBatch x) = R.runRedis conn (R.multiExec x)
来暴露 monadic 接口,并且包含我的基本操作的类,包含我的两个RedisCmd
和RedisBatch
类型的实例。
RedisCmd
现在,类型为class Redis f where
get :: ByteString -> f (Maybe ByteString)
set :: ByteString -> ByteString -> f (R.Status)
的计算可以适用于任何一种行为(事务性或非事务性),但那些需要monad (Applicative f, Redis f) => ...
的计算只能在非事务模式下运行。 / p>
当所有人说完了,我不相信这是值得的。人们似乎喜欢在这样的库之上构建抽象,总是提供比库更少的功能,并编写更多代码来隐藏bug。每当有人说“我可能想要切换数据库”时,我感叹:这是唯一足够抽象的抽象目的是不提供任何功能。担心在需要的时候(也就是永远)切换数据库。
另一方面,如果您的目标不是抽象数据库而只是为了清理界面,那么最好的办法就是分叉库。