daemons-0.1.2: examples/Memo.md
Memo
====
> A simple in-memory key-value store
Welcome to the first `daemons` tutorial, in which we walk through
writing an in-memory key-value store with an RPC-like interface. The
code for this tutorial is
[Memo.hs](https://github.com/scvalex/daemons/blob/master/examples/Memo.hs).
Concretely, we want a program such that:
- `memo put x 42` associates the value `42` with the key `x`, and
- `memo get x` returns the value `42`.
First of all, the extensions and imports:
{-# LANGUAGE DeriveGeneric, OverloadedStrings #-}
We need `DeriveGenerics` for
[cereal](http://hackage.haskell.org/package/cereal) to generate
serializers and deserializers automatically, and we enable
`OverloadedStrings` because it makes working with `ByteString`s much
nicer.
module Main where
import Control.Concurrent.MVar ( MVar, newMVar, modifyMVar )
import Data.ByteString.Char8 ( ByteString )
import Data.Default ( def )
import Data.Serialize ( Serialize )
import Data.String ( fromString )
import qualified Data.Map as M
import GHC.Generics
import System.Environment ( getArgs )
import System.Daemon
Our key-value store will be a `Map ByteString ByteString` and we'll
store it in an `MVar` to synchronize concurrent accesses. Instead of
handcrafting a binary protocol for our daemon, we take the easy road
and generate it automatically with `Data.Serialize` and
[GHC.Generics](http://www.haskell.org/ghc/docs/7.4.2/html/users_guide/generic-programming.html).
We import `System.Daemon` which is the high-level interface to the
`daemons` library. The daemons' configuration is an instance of
[Data.Default](http://hackage.haskell.org/package/data-default), so
we'll be able to use the defaults.
data Command = Put ByteString ByteString
| Get ByteString
deriving ( Generic, Show )
instance Serialize Command
We define a datatype for the `put <key> <value>` and `get <key>`
commands. We let GHC derive the `Generics` instance, which gives us a
pure Haskell representation of the type; this is used by the
`Serialize` instance to generate all the necessary binary
serialization and deserialization code.
data Response = Failed String
| Value ByteString
deriving ( Generic, Show )
instance Serialize Response
Similarly, we define a datatype for the possible responses. These can
either be values requested by `get <key>`, or failure messages.
type Book = M.Map ByteString ByteString
handleCommand :: MVar Book -> Command -> IO Response
handleCommand bookVar comm = modifyMVar bookVar $ \book -> return $
Our "book" is just a map of `ByteString`s; our command handler takes
this map and a command, and returns a response.
Whenever the daemon receives a command, it spawns a new thread and
runs the command handler. We want to share the book between these
concurrent calls to the handler, so we stick it in an
[MVar](http://www.haskell.org/ghc/docs/7.4.2/html/libraries/base/Control-Concurrent-MVar.html).
An `MVar` is basically a thread-safe box which holds at most *one*
item. We use `modifyMVar` which takes the book out of the `MVar`,
runs our function with it, and puts the returned book back in the
`MVar`.
case comm of
Get key -> ( book
, maybe (Failed "not found") Value (M.lookup key book) )
A `get <key>` command does not change the book, so we just return it.
We look up the key and return its value or a failure message.
Put key value -> ( M.insert key value book
, Value "ok" )
A `put <key> <value>` command inserts the key-value pair into the
book, and returns a confirmation message.
main :: IO ()
main = do
bookVar <- newMVar M.empty
let options = def { daemonPort = 7856 }
ensureDaemonRunning "memo" options (handleCommand bookVar)
Before doing anything else, we need to ensure that the daemon is
running: we create an empty book, customize the daemon's default
options, and finally start it. Note that `ensureDaemonRunning` checks
if the daemon is running and starts it otherwise; so, the daemon will
be started the first time the program is run, and all later runs will
use the initial daemon.
args <- getArgs
let args' = map fromString args
Now it's time to handle the user input. First, we convert all the
arguments to `ByteString`s for ease of use.
res <- case args' of
["get", key] -> runClient "localhost" 7856 (Get key)
["put", key, value] -> runClient "localhost" 7856 (Put key value)
_ -> error "invalid command"
Next, we parse the arguments into a command and send it to the daemon.
We call `runClient` with the port we gave earlier to
`ensureDaemonRunning` and with the parsed command.
print (res :: Maybe Response)
Finally, we print the returned response. Note that `runClient` is
polymorphic in its return so we *need* to specify the type of the
response.
Now let's see it in action:
{-
% dist/build/memo/memo get apples
Daemon started on port 7856
Just (Failed "not found")
% dist/build/memo/memo put apples 23
Just (Value "ok")
% dist/build/memo/memo get apples
Just (Value "23")
-}
To recap, we:
- wrote data-types for commands and responses and gave them
`Serialize` instances,
- wrote a handler that takes a command and returns a response,
- ensured that our daemon is running with `ensureDaemonRunning`, and
- sent commands and received responses with `runClient`.
This tutorial illustrates the basic concepts behind `daemons`, but
hides a powerful feature: the interface is *streaming*. See the
[Queue](https://github.com/scvalex/daemons/blob/master/examples/Queue.hs)
(Poor Man's Task Queue) tutorial for an example use of the streaming
interface.