packages feed

happs-tutorial-0.8.1: src/FirstMacid.hs

{-# LANGUAGE TemplateHaskell, DeriveDataTypeable,
  MultiParamTypeClasses, TypeFamilies, FlexibleContexts #-}

module Main where

import Happstack.State
import Data.Typeable
import Control.Monad.State
import Control.Monad.Reader
import Control.Concurrent (MVar)

-- First we define our basic datatype.  In this case, it is simply
-- a wrapper around an Int.
data ExampleState = ExampleState Int
    deriving (Typeable, Show)

{- Here we accept the default for Version, which corresponds to a version number of 0
   and no previous versions of the state defined.
   You might be saying "Hold on, there, what's all that about?".
   I'm going to say "Oh look, a Unicorn!" and hope you remain distracted until the
   chapter on versions & migrations. -}

instance Version ExampleState

-- This is a bit of lovely Template Haskell hackery that you really
-- shouldn't think too hard about.  Suffice to say, you need to call this.
$(deriveSerialize ''ExampleState)

{- Now we're getting into the real meat of our application!
   The Update and Query methods that we can use to actually
   manipulate the persistent state. 

   The two important points are that Update st is a 
   MonadState st and that Query st is a MonadReader st.
   
   If you have any familiarity with the mtl library, you can
   apply all your normal intuition about Reader & State monads
   to writing updates and queries.
-}

-- A very simple Update that just increments the state
succVal :: Update ExampleState ()
succVal = modify (\(ExampleState n) -> ExampleState (n+1))

-- A simple Query that just returns the bare Int contained in the state
getVal :: Query ExampleState Int
getVal = do
  ExampleState n <- ask
  return n

-- An Update that actually takes an argument.  That may not seem odd
-- at the moment, but we'll talk more about it in a second.
incVal :: Int -> Update ExampleState ()
incVal i = modify (\(ExampleState n) -> ExampleState (n+i))

-- An Update that makes use of the utility function getRandomR.
-- getRandom or getRandomR can be used in the middle of an Update
-- or Query without needing to perform any unsafe IO magic or somehow
-- carry around a random seed with your state.
-- This Update also serves as a reminder that despite all the deep
-- magic that is used to make Updates and Querys work, these are
-- still legitimate Haskell monads and we can combine them together
-- the same as always.

succRand :: Update ExampleState ()
succRand = getRandomR (0,1000) >>= incVal

{- Another required bit of Template Haskell magic.  This will derive
   a set of data types to be used in the over all event handling that
   are in 1-1 correspondence with the Updates and Querys that you
   defined up above.  Try compiling this with the -ddump-splices option
   in order to actually take a look at what gets created by mkMethods.
   
   The data types will have the same name as your functions except
   with a capitalized first letter.

   The parameters to your Update or Query become the parameters to the
   constructor of your data type.  So in this case incVal becomes
   IncVal Int.
-}

$(mkMethods ''ExampleState ['succVal, 'getVal, 'incVal, 'succRand])

-- One more thing you can't forget to define in order for MACID to work
-- If your data type is composed from non-primitive data, then you need
-- to build up a type level list using the constructor :+: and the
-- empty dependency End.  Another example is going to cover the use of sub components 
-- and when you'd want a non-trivial Dependencies type.
instance Component ExampleState where
    type Dependencies ExampleState = End
    initialValue = ExampleState 0

{-
  The way you actually start running a Happstack.State
  application is to call startSystemState and pass it
  a Proxy.  As you can see below in the definition of
  rootState, the Proxy type carries no actual data,
  it is simply a carrier of the type of your state.
 
 startSystemState is the function you'll most often use
 for Happstack.State clients.  It uses the default
 behavior of serializing data to files under _local.
 While this isn't the only option for serialization of
 state, it is the only one we will consider in this
 tutorial.
-}
rootState :: Proxy ExampleState
rootState = Proxy

main :: IO ()
main = startSystemState rootState >>= commandLoop

commandLoop :: MVar TxControl -> IO ()
commandLoop ctl = do
  putStrLn "Enter 'i' to increment the state by a number of your choosing."
  putStrLn "Enter 'v' to view the state."
  putStrLn "Enter 's' to increment the state by 1."
  putStrLn "Enter 'c' to checkpoint the state."
  putStrLn "Enter 'r' to increment the state by a random number."
  putStrLn "Enter 'q' to quit."
  val <- liftM head getLine
  handler ctl val

{-
  At last, our handler function is quite simple and bare bones.
  If you experiment with it at the command line you should able
  to watch it work.  There isn't much in the way of a UI here,
  but it's enough that you can get the flavor of how one uses
  Happstack.State in the main application logic.

  The two functions I'd like to discuss are query and update.
  They operate fairly similarly to each other.

  update :: (MonadIO m, UpdateEvent ev res) => ev -> m res
  query :: (MonadIO m, QueryEvent ev res) => ev -> m res

  You, basically, pass data corresponding to Query functions
  to query and Updates to update.  That's really what the
  type context about UpdateEvent and QueryEvent comes down
  to.  The really nice thing is that they work for any
  MonadIO.  In the example below, we just use IO itself, but
  since ServerPartT has a MonadIO instance we can very
  seamlessly use update & query in our Happstack web apps.

  Now, we're explicitly threading through the MVar TxControl
  returned by our call to startSystemState into the handler.
  It'd probably be cuter to just use a ReaderT (MVar TxControl) IO
  but I want to keep this code extremely straight forward.
  The only place where we actually need this TxControl is when
  calling the createCheckpoint function.

  createCheckpoint does exactly what it sounds like.  It
  serializes a checkpoint of the application state.  
  You might be wondering what a checkpoint does if the state
  is already persistent.  Normally Happstack.State serializes
  the event log, which is played back whenever the application
  is restarted.  When you create a checkpoint, you are 
  serializing out a state you can use as a starting point
  for your application when it is restarted.  You might
  want to try the following outline to build your intuition
  for checkpoints.

  1. Increment the state a few times
  2. Create a checkpoint
  3. Increment the state again
  4. Close down the app and examine the
     _local directory.  You should see
     the files corresponding to the checkpoint
     and the events recorded after it
  5. Restart the app.  It should be right where
     you left it.  Close it again.
  6. Delete the events files
  7. Restart the app.  The state should correspond
     to the checkpoint you took.
  8. ???
  9. Profit!
-}

handler :: MVar TxControl -> Char -> IO ()
handler _ 'q' = return ()
handler c 'v' = query GetVal >>= print >> commandLoop c
handler c 's' = update SuccVal >> commandLoop c
handler c 'i' = flip catch (const $ commandLoop c) $ do
                v <- readLn
                update $ IncVal v
                commandLoop c
handler c 'c' = createCheckpoint c >> commandLoop c
handler c 'r' = update SuccRand >> commandLoop c
handler c _ = commandLoop c