I'm attempting to define an abstract interface for a key value store.

Some requirements:

  • Once a key has been written it should never be overwritten (although it may be deleted
  • Transactions with multiple operations are not required

To begin with, this is the basic interface:

class KVS a where
  get :: ByteString -> a -> Maybe ByteString
  put :: ByteString -> ByteString -> a -> Maybe a
  delete :: ByteString -> a -> a

And here's an InMemory instance:

newtype InMemoryKVS = InMemoryKVS (M.Map ByteString ByteString)

instance KVS InMemoryKVS where
  get k (InMemoryKVS m) = M.lookup k m
  put k v (InMemoryKVS m)
    | M.Member k m = Nothing
    | otherwise = Just $ InMemoryKVS $ M.insert k v m
  delete k (InMemoryKVS m) = InMemoryKVS $ M.delete k m

I would also like to write instances that use the IO monad, so presumably the typeclass needs to take this into account. Here's my first attempt at that:

class IOKVS a where
  ioGet :: ByteString -> a -> IO (Maybe ByteString)
  ioPut :: ByteString -> ByteString -> a -> IO (Maybe a)
  ioDelete :: ByteString -> a -> IO a

instance IOKVS InMemoryKVS where
  ioGet k s = return $ get k s
  ioPut k v s = return $ put k v s
  ioDelete k s = return $ delete k s

Here is how I'm currently using it:

{-# LANGUAGE OverloadedStrings #-}
main :: IO()
main = do
  kvs <- return (InMemoryKVS M.empty)
  kvs' <- ioPut "foo" "bar" kvs
  maybe (putStrLn "put failed") (printKey "foo") kvs'

printKey :: IOKVS a => B.ByteString -> a -> IO()
printKey k s = do
  v <- ioGet k s
  maybe (putStrLn "doesn't exist") (\v -> B.putStrLn v) v

I feel that there is a lot of room for improvement with my design. Here are some questions that I have:

  1. In main I'm manually keeping track of the state of kvs. Could my code be simplified by using the State monad? If so how can I manage the complexity of having IO, State and Maybe stacked?
  2. I'm not happy with the design of the IOKVS typeclass. Specifically I don't think it will work well with concurrent access.
  3. Can this design hide a connection pool to an external db?
  4. Is there a more elegant way to handle the failure cases then using the maybe function? I would like to be able to compose these into higher level actions that may fail, but not have to deal with the failure case within each action.
  5. Can you see any other areas for improvement?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.