Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Building a monad on top of hedis, a haskell redis lib

Tags:

haskell

redis

I want to write a simple DSL on top of hedis, a redis lib. The goal is to write functions like:

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

The basic idea is to fetch data from redis, put it in a state monad, do some stuff with the state and then put the updated state back into redis.

Obviously, it should be atomic so load and put should happen in the same Redis transaction. Hedis permits that by wrapping calls to Redis in a RedisTx (Queued a). For example, we have get :: ByteString -> RedisTx (Queued a). Queued is a monad and you then run multiExec on your Queued a to execute everything in the Queued a in the same transaction. So I tried to define my MyRedis as such:

import qualified Database.Redis as R
newtype MyRedis a = MyRedis { runMyRedis :: StateT MyState R.RedisTx a } -- deriving MonadState, MyState...

The run function calls multiExec so I'm sure that as long as I stay in MyRedis everything happens in the same transaction.

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

Furthermore, I can define commit as:

commit :: ByteString -> MyRedis (R.Queued R.Status)
commit bs = do
  MyState new <- get
  (MyRedis . lift) (R.set bs new)

And a computation would look like:

computation :: MyRedis (R.Queued R.Status)
computation =  do
  load gid
  MyState bs <- get
  put $ MyState (reverse bs)
  commit gid
  where gid = "123"

But I can't figure out how to write "load"

load :: ByteString -> MyRedis ()
load gid = undefined

Actually, I think that it is not possible to write load, because get is of type ByteString -> RedisTx (Queued (Maybe ByteString)) and I have no way to peek into the Queued monad without executing it.

Questions:

  1. Is it correct that because of the type of Hedis's get, it doesn't make sense to define a load function with the semantics above?

  2. Is it possible to change the MyRedis type definition to make it work?

  3. Hedis doesn't define a RedisT monad transformer. If such a transformer existed, would it be of any help?

  4. Hedis defines (but does not export to lib users) a MonadRedis typeclass; would making my monad an instance of that typeclass help?

  5. Is it the right approach? I want to:

    • Abstract over Redis (I may switch someday to another DB)
    • Restrict the Redis functions available to my users (basically only lifting to MyRedis get and set)
    • Guarantee that when I run my monad everything happens in the same (redis) transaction
    • Put my redis abstraction at the same level as other functions in my monad

You can play with the code at http://pastebin.com/MRqMCr9Q. Sorry for the pastebin, lpaste.net is down at the moment.

like image 397
cpa Avatar asked Sep 06 '25 06:09

cpa


1 Answers

What you want is not possible. In particular, you can't provide a monadic interface while a running a computation in one Redis transaction. Nothing to do with the library you're using - it's just not something Redis can do.

Redis transactions are rather different from the ACID transactions you may be used to from the world of relational databases. Redis transactions have batching semantics, which means that later commands cannot in any way depend on the result of earlier commands.

Look: here's something similar to your example, run at the Redis command line.

> 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

Anyway, that's the purpose of that library's slightly odd Queued type: the idea is to prevent you from accessing any of the results of a batched command until the end of the batch. (It seems that the author wanted to abstract over batched and non-batched commands but there are simpler ways to do that. See below for how I'd simplify the transactional interface.)

So there's no "choosing what to do next" when Redis transactions are involved, but the whole point of (>>=) :: m a -> (a -> m b) -> m b is that later effects can depend on earlier results. You have to choose between monads and transactions.


If you decide you want transactions, there's an alternative to Monad called Applicative which handlily supports purely-static effects. This is exactly what we need. Here's some (entirely untested) code illustrating how I'd cook an Applicative version of your idea.

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)

If I wanted to abstract over transactional-or-not behaviour, as the library author has attempted to do, I'd write a second type RedisCmd exposing a monadic interface, and a class containing my primitive operations, with instances for my two RedisBatch and RedisCmd types.

class Redis f where
    get :: ByteString -> f (Maybe ByteString)
    set :: ByteString -> ByteString -> f (R.Status)

Now, computations with a type of (Applicative f, Redis f) => ... could work for either behaviour (transactional or not), but those which require a monad (Monad m, Redis m) => ... would only be able to run in non-transactional mode.


When all's said and done, I'm not convinced it's worth it. People seem to like building abstractions over libraries like this, invariably providing less functionality than the library did and writing more code for bugs to lurk in. Whenever someone says "I may want to switch databases" I sigh: the only sufficiently abstract abstraction for that purpose is one which provides no functionality. Worry about switching databases when the time comes that you need to (that is, never).

On the other hand, if your goal is not to abstract the database but just to clean up the interface, the best thing may be to fork the library.

like image 121
Benjamin Hodgson Avatar answered Sep 07 '25 20:09

Benjamin Hodgson