在hedis上建立一个monad,一个haskell redis lib

时间:2016-02-24 18:48:47

标签: haskell redis

我想在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中。

显然,它应该是原子的,因此loadput应该在同一个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执行它。

问题:

  1. 正确的是,由于Hedis获取的类型,用上面的语义定义load函数是没有意义的吗?

  2. 是否可以更改MyRedis类型定义以使其有效?

  3. Hedis没有定义RedisT monad变换器。如果存在这样的变压器,它会有任何帮助吗?

  4. Hedis定义(但不导出到lib用户)MonadRedis类型类;会让我的monad成为那个类型类帮助的一个实例吗?

  5. 这是正确的做法吗?我想:

    • Redis上的摘要(我可能有一天会切换到另一个数据库)
    • 限制我的用户可以使用的Redis功能(基本上仅提升到MyRedis getset
    • 保证当我运行我的monad时,一切都发生在同一个(redis)事务中
    • 将我的redis抽象放在与monad中其他函数相同的级别
  6. 您可以使用http://pastebin.com/MRqMCr9Q处的代码。抱歉,对于pastebin,lpaste.net目前正在关闭。

1 个答案:

答案 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 接口,并且包含我的基本操作的类,包含我的两个RedisCmdRedisBatch类型的实例。

RedisCmd

现在,类型为class Redis f where get :: ByteString -> f (Maybe ByteString) set :: ByteString -> ByteString -> f (R.Status) 的计算可以适用于任何一种行为(事务性或非事务性),但那些需要monad (Applicative f, Redis f) => ...的计算只能在非事务模式下运行。 / p>

当所有人说完了,我不相信这是值得的。人们似乎喜欢在这样的库之上构建抽象,总是提供比库更少的功能,并编写更多代码来隐藏bug。每当有人说“我可能想要切换数据库”时,我感叹:这是唯一足够抽象的抽象目的是不提供任何功能。担心在需要的时候(也就是永远)切换数据库。

另一方面,如果您的目标不是抽象数据库而只是为了清理界面,那么最好的办法就是分叉库。