Sunday, April 21, 2013

More on Session management in MFlow: getSessionData

In the previous post "controlling backtracking in MFLow" I told about how the backtracking mechanism match the request coming from a page in the navigation history with the piece of code that generated it, so that the flow could proceed from this point on. ask will backtrack to the previous ask until the parameters in the request match. This ask statement contains a computation in the View monad with a closure that contains the variable values that where used when the page was created. This is tbe essence of the backtracking mechanism. In a continuation-based framework, such is osigen from Ocaml or seaside from Smalltalk, all the navigation is exposed to the web server, so the request goes directly to the intended closure. In MFlow there is a single entry point which is a running process that will backtrack to the ask statement that match with the page if necessary. If the process is not running, it will be restarted.

However sometimes, the back button is used for navigation and we want to keep the state, not to roll it back. In the previous example of the shop, the cart is passed as a parameter. That may be tedious if the flow is complicated. To solve these two problems, I created getSessionData and setSessionData. which stores and retrieves user-defined data in the session, by means of a map indexed by data type. In this way the user don´t need to pass its application data by parameters, neither it need to embed its own state monad. But also, the programmer when pressing back, can choose to backtrack to previous state values  or not, depending on the nature of the flow.

This session data works in the Flow monad as well as the View monad. That means that it can be used in both sides of ask. Since when backtracking the only code executed is the View monadic code behind ask, getSessionData,  when it is in View monad, will retrieve the last state no matter if it is on backtracking. This is the way to keep the user-defined state actualized to the last value when backtracking.

In the example below, the shopping cart is stored as session data. In the ask statement that show the shopping cart, getSessionData is used in the View monad (in bold), so when backtracking  the code will get the last shopping cart, so no roll-back effect will appear and the back button can be used for navigation purposes.  Here is a  loop with an alternance between buying from the catalog and showing the shopping cart. The loop, and the navigation can be executed forward, by pressing the links, and backward, by means of the back button. The result is the same.

If getSessionData is not in the View monad, and the cart variable in scope is used, then this variable will keep its value that it had at each moment of the loop, so when going back we will see fewer and fewer items in the shopping cart.

Here step is used, so the state is persistent.  setTimeouts will kill the process after two minutes. After one month, if the user has not returned, the state will be erased and the shopping  cart will be empty. This is the complete program:

 
module Main where
import MFlow.Wai.Blaze.Html.All 
import Data.Typeable
import Data.String(fromString)

import qualified Data.Vector as V

main= do
   addMessageFlows  [("", runFlow $ shop ["iphone","ipad","ipod"])]
   wait $ run 8081 waiMessageFlow

shop products= do
   setHeader $ html . body
   setTimeouts 120 (30*24*60*60)
   catalog
   where

   catalog = do
           bought <- step . ask $ showProducts products
           cart <- getSessionData `onNothing` return emptyCart
           let n = cart V.! bought
           setSessionData $ cart V.// [(bought,n+1)]
           step $ do
             r <- ask $  do
                   cart <- span=""> getSessionData `onNothing` return emptyCart
                   p << showCart cart ++> wlink True << b << "continue shopping"
                                      <|> wlink False << p << "proceed to buy"


             if( r== False) then ask $ wlink () << "not implemented, click here"
                          else return ()

           catalog

   emptyCart= V.fromList $ take (length products) (repeat  (0::Int))

   showProducts xs= firstOf $ map (\(i,x) -> wlink i (p <<  x)) $ zip  [0..] xs 
 
 
MFlow : http://github.com/agocorona/MFlow
 
 

No comments: