Wednesday, April 24, 2013

MFlow: What about the data tier? Adding it to the shopping example

In the previous post I created an skeleton of a shopping application where the back button in the web browser is used for navigation, instead of a way to undo a transaction.  The purpose here is to add a small database of products, to search the database by different ways. This is the original  program, where three products are hardcoded:

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 <- 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

 


First, some headers:

{-# LANGUAGE DeriveDataTypeable #-}
module Main where
import MFlow.Wai.Blaze.Html.All as MF
import Data.Typeable
import Data.String(fromString)
import Data.TCache.DefaultPersistence
import Data.TCache.IndexQuery as Q
import Data.TCache.IndexText
import Data.Maybe
import qualified Data.Map as M
import Control.Workflow.Configuration
import Data.Text.Lazy as T


Lets define a product, and we will create some of them:

type ProductName= String
type Quantity= Int
type Price= Float


data Product= Product{ namep :: ProductName
                     , typep :: [String]
                     , descriptionp :: String
                     , pricep :: Price
                     , stock :: Int}
              deriving (Read,Show,Typeable)



createProducts= atomically $ mapM newDBRef
    [ Product "ipad 3G"   ["gadget","pad"]   "ipad 8GB RAM, 3G"       400 200
    , Product "ipad"      ["gadget","pad"]   "ipad 8 GB RAM"           300 300
    , Product "iphone 3"  ["gadget","phone"] "iphone 3 nice and beatiful"  200 100
    ]



We will use TCache for persistence.  It adds STM transactions, and user defined persistence. It also bring default persistence in files, that is what I will use now.

newDBRef (above) creates a database reference and the record pointed too. To do so  we need only to define the indexable instance for the record, that defines a unique key:

instance Indexable Product where
   key prod= "Prod "++ namep prod


We need to search by the product name namep, to search product by types typep and to perform text search in the description descriptionp.

main= do
   Q.index namep                          -- for field indexation
   indexList typep (Prelude.map T.pack)   -- for list indexation
   indexText descriptionp T.pack          -- for text indexation


These indexation statements create triggers that inspect creations and modifications of these fields in the registers.  (Use http://holumbus.fh-wedel.de/hayoo/hayoo.html for fast documentation about these and other primitives used in the application)

Then, we create the products one and one single time.

   runConfiguration "createprods" $ once createProducts

runConfiguration is a Workflow functionality for configuration. The purpose is to execute (once)  things one and one single time.

And the start of the shop, that will be defined later:

   addMessageFlows  [("", runFlow shop )]
   wait $ run 8081 waiMessageFlow



Lets redefine the shopping cart. Instead of a Vector, we will have the product name, the quantity and the unit price in a map:

type Cart= M.Map ProductName (Quantity, Price)
showCart :: Cart -> String
showCart = show


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


The catalog loop will do the same than in the previous static application: The user choose a product from the catalog, the shopping cart will be shown with the new product and so on:

   catalog = do       
       bought <- buyProduct
       shoppingCart bought
       catalog


Now the reservation (buyProduct) and the shoppingCart processing is more complex and with more steps:

 Let's tell something about Workflows and step. This statement writes the result of a computation in a log. When the program is restarted, each step call will read an entry in the log and return it, instead of executing the computation.  So the program will execute the steps already logged, so at the end of the log, the process instruction pointer is located after the last step saved. the shop computation will need to remember the state of the shopping cart after restar from a timeout set with setTimeouts (see previous posts about this), so it need to use step.

buyProduct return a product name. Now there are two options: Either a search for products or a navigation trough product types. To obtain the types, it is necessary to get all of them from all the products. This is what allElemsOf does.

   atomic= liftIO . atomically
   showList []= wlink Nothing << p << "no results"
   showList xs= Just <$> firstOf [wlink  x << p <<  x | x <- xs]

   buyProduct = step $ do
        ttypes   <atomic $ allElemsOf typep
        let types= Prelude.map T.unpack ttypes
        r  <- ask $   h1 << "Product catalog"
                  ++> p << "search" ++> (Left <$> getString Nothing)
                  <|> p << "or choose product types" ++>  (Right <$> showList types)


So r will have either the search string (by getString) or  what showList produces. And what it produces is Nothing if the list of types is empty or one of the links pressed -wlink- corresponding to a product name. firstOf  applied to a list of widgets, return the one activated by the user.

And now given either the search string or the type of product, it is necessary to read the product that meet the condition in the database:

   prods <case r of
      Left str           -> atomic $ Q.select namep $ descriptionp `contains` str
      Right (Just type1) -> atomic $ Q.select namep $ typep `containsElem` type1
      Right Nothing      -> return []


Here there are two kind of queries: the first is the names of products which contains the search terms in the description field. The second query is all the products that include the type in the typep field

The result is the list of product names. If no  search result of no types of products,  we return to the page again:


      if Prelude.null prods then buyProduct else do

                let search= case r of
                    Left str ->    "for search of the term " ++ str
                    Right (Just type1) -> "of the type "++ type1

        r <- ask $ h1 << ("Products " ++ search) ++> showList prods
        case r of
              Nothing   -> buyProduct
              Just prod -> breturn prod


Here showList present the products as links. so r will return the chosen product or Nothing if there was no result for the previous query for products (It is not the case, since the that has been ruled out in the first line).

Now, the shoppingCart. First the shopping cart is retrieved form the session context with getSessionData (see the previous post). then, to know the price, the register with this name is retrieved. Then the cart is updated.

shoppingCart bought= do
       cart <- getSessionData `onNothing` return (M.empty ::Cart)
       let (n,price) = fromMaybe (0,undefined) $ M.lookup  bought cart
       (n,price) <- step $ do

                  if n /= 0 then return (n,price) else do
                    [price] <- atomic $ Q.select pricep $ namep .==. bought
                    return (n, price)
       setSessionData $ M.insert  bought (n+1,price) cart


Since namep determine the main key (see the Indexable instance), we would substitute the query

[price] <- atomic $ Q.select pricep $ namep .==. bought

By

price <- readResource Product{ namep= bought} >>= return . pricep . fromJust

Which is quite faster. So, really,  there is no need to index the main key. But it has been done in this  case for a matter of example. readResource gets an incomplete object with defined key and returns Maybe the complete register.

Finally, the shopping cart is visualized:

       step $ do
         r <- ask $ do

              cart <- getSessionData `onNothing` return (M.empty :: Cart)
              h1 << "Shopping cart:"
                ++> p << showCart cart
                ++> wlink True  << b << "continue shopping"
                <|> wlink False << p << "proceed to buy"


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

 breturn  means that the procedure return, but may be called back when backtracking as a result of the button back pressed in the web browser.

Here there is no real "buy" operation implemented. The naming as "buy" operations what are really reservations of products in a shopping cart are just in order to maintain the names of the previous version. Actually, they are shopping cart reservations. Buy It means to modify the stock of the product in the database and avoid to roll-back when backtracking. I will do it in the next post.

 The code above is complete. This example is embedded in this source file:

https://github.com/agocorona/MFlow/blob/master/Demos/loginUserPage.hs

 The MFlow package:
https://github.com/agocorona/MFlow



Post a Comment