Monday, February 25, 2008

Using HAppS-State

Update: A demo of the finished application is now available. See this post for more information.

In the last post, I outlined the requirements for making a data type an instance of Component. This is great, but not very useful without a mechanism for accessing the state data.

HAppS persists its state by storing functions that operate on the state. This requires a way to serialize the functions. HAppS does this for you with the TemplateHaskell function mkMethods. So how does this affect you? Your functions that manipulate state must be either Update or Query functions. Update functions use the State monad, and Query functions use the Reader monad.

First we'll set up some convenience functions that will be used to construct the actual Query and Update functions.

> askUsers :: MonadReader State m => m (M.Map String User)
> askUsers = return . users =<< ask
> 
> askSessions::MonadReader State m => m (Sessions SessionData)
> askSessions = return . sessions =<< ask
> 
> modUsers :: MonadState State m =>
>             (M.Map String User -> M.Map String User) -> m ()
> modUsers f = modify (\s -> (State (sessions s) (f $ users s)))
> 
> modSessions :: MonadState State m =>
>                (Sessions SessionData -> Sessions SessionData) -> m ()
> modSessions f = modify (\s -> (State (f $ sessions s) (users s)))

Now we need functions for adding, listing, authenticating, and checking the existence of users. All the code dealing with the State and Reader monads has been hidden away in the above convenience functions, so these should be pretty easy to understand.

> isUser :: MonadReader State m => String -> m Bool
> isUser name = liftM (M.member name) askUsers
> 
> addUser :: MonadState State m => String -> User -> m ()
> addUser name u = modUsers $ M.insert name u
> 
> authUser :: MonadReader State m => String -> String -> m Bool
> authUser name pass = do
>                      u <- liftM (M.lookup name) askUsers
>                      liftM2 (==) (liftM password u) (return pass)
> 
> listUsers :: MonadReader State m => m [String]
> listUsers = liftM M.keys askUsers

Here are the functions for manipulating sessions. These are based on AllIn.hs. numSessions is a little different from the others. Here is the explanation from the AllIn comments:

Numsessions takes a proxy type as an argument so we know which session you want. You may have sessions on more than one type in state operating or sessions may be nested elsewhere. You can only have one of each type in all of state.
> setSession :: (MonadState State m) => SessionKey -> SessionData -> m ()
> setSession key u = do
>   modSessions $ Sessions . (M.insert key u) . unsession
>   return ()
> 
> newSession u = do
>   key <- getRandom
>   setSession key u
>   return key
> 
> getSession::SessionKey -> Query State (Maybe SessionData)
> getSession key = liftM ((M.lookup key) . unsession) askSessions
> 
> numSessions:: Proxy State -> Query State Int
> numSessions = proxyQuery $ liftM (M.size . unsession) askSessions

Now that we have all our state functions, we just need to call mkMethods. These functions may be accessed from any IO with something like (query $ authUser name pass) or (update $ AddUser name user)

> $(mkMethods ''State ['addUser, 'authUser, 'isUser, 'listUsers,
>             'setSession, 'getSession, 'newSession, 'numSessions])

Summary: We have seen that access to HAppS state is accomplished using the Reader and State monads. These accessor functions must be passed through mkMethods so HAppS knows how to serialize them for storage.

When you append this post to the previous one and add MonadState to the list of imports for Control.Monad.State, it should compile. This gives us everything we need to manage our authentication framework's state with HAppS.

3 comments:

nomeata said...

Just wondering: Is there a reason you use

return . sessions =<< ask

instead of

asks sessions

BTW, great tutorial!

mightybyte said...

I took that code from AllIn.hs in the HAppS-HTTP examples, and that's the way it was done there. But you're right, "asks sessions" is much simpler.

Bayle said...

Perhaps where you say, "...something like (query $ authUser name pass)", you mean, "something like (query $ AuthUser name pass)" (the difference is the capitalization of AuthUser)?