Wednesday, August 14, 2013

How to use backtracking to present the main menu on every page for free

Still I´m exploring the possibilities of expressing the navigation of a web site by means of matching and backtracking. Using a monad with backtracking, and link + form parameters as the elements for the matching mechanism. That is how MFlow handles the web navigation. I increasingly find that this is the right paradigm that will be the standard if future web development. Event handling coding and all their abstruse configurations and hacks for proper state managemnt will be contemplated as the legacy of an ominous past  that the Humanity had to travel before finding the freedom-under-control of the monadic utopia  ;)       

As an example of how natural is the transformation of web navigation requirements in terms of tracking and backtracking in a monadic, imperative-like code, I show you how I solved my last requirement:

I want to have the menu present in every page, in my demo at:  

http://mflowdemo.herokuapp.com

Now it has the main menu available for every page. Additionally, all the examples, including the persistent one (the shopping cart) are in a single flow.

I could have done thr first by just adding the menu as a widget more in each page, but this means that my code has to check for clicks in the menu on every page, besides the concrete code that I´m focused on.

So, if i have this code

r ←  page pagecode            (1)
normalAppflow r

To add a menu to each page I have to change the code in a way that look like:

r ←  ask $ fmap Left menu <|> fmap Right pagecode
case r of
   Right x        → normalAppflow x
   Left  menuitem → processitem menuitem

Or alternatively:
r ←  ask $ menu `waction` processItem **> pagecode
normalAppflow r

With:

processitem item= case item of item1 -> ... ...

In both cases, the menu items would be executed recursively. That is not good for the memory usage of the application since, to allow backtracking, the Flow monad is not tail recursive. The memory is freed when the timeout expires, but still it is not the best solution. It would be better backtrack to the menu before processing the option, so the data of this abandoned branch would be garbage collected.

Alternatively, I can put each demo in a page flow, where each page in the demo becomes a auto-refreshed widget under a single main page together with the menu, but that denaturalize some demos that are inherently made for page navigation. Additionally I don´t want to use such advanced thing as a page flows and auto-refreshing in the home page of a demo that I want to keep as simple and understandable as possible.

So I tried to use the backtracking mechanism in a way that when an item in the menu is clicked in any demo page, instead of checking for it and call again the menu, It backtracks to the menu page, where the flow will track the appropriate branch of execution depending on the menu item chosen.

Now I want not to code this manually, so instead I make my menu tell ask that he is some pages back and will care for the response, so page must initiate a backtraking. This is done with retry:

retry w= w >> modify (\st -> st{inSync=False})

inSync is an internal state parameter. It means that the server is in sync with the browser because the server found a parameter or link sent by the browser that match with the page that the server is now processing. because ask/page is forced to False by retry, he initiates a backtracking until some previous page match the web browser response.

Now the code becomes:

r ←  ask $ retry menu **> pagecode
normalAppflow r

or
r ←  pagem pagecode
normalAppflow r 

which is almost the same than the original code in (1).

With:

pagem pagecode= ask $ retry menu **> pagecode 

The **> operator is the applicative *> but the first ever executes the second parameter no matter if the first succeeded or not. Since all my pages use the same menu, then I can substitute ask and page by pagem, that knows implicitly about the menu. With this exception I have nothing more to change in my application. if no link of form of pagecode is clicked, then page will find itself not in sync, but actually, there have been a request for a link/form in the menu that will be handled by the menu page, back and down in the execution tree. That is why retry is called as such.

A page can have as many retried widgets as you like. It is important to have the retried widgets cached, since cached widgets maintain the parameter numbering for the web forms and the link deep appropriate for the place where they backtrack. Moreover a menu used in many pages is an inherent candidate for being cached, for performance reasons. I though about making these requirement explicit in the type system, but at this moment I find this alternative a bit overengineered.

This is how the navigation monad example would look like with these modifications. The essential changes are in bold:
import MFlow.Wai.Blaze.Html.All
        
main= runNavigation "" . transientNav $ do
  option ←  ask  menu1 
  case option of
    "1" → do
           pagem $ wlink "2" << contentFor "1"
           pagem $ wlink "3" << contentFor "2"
           pagem $ wlink "4" << contentFor "3"

    "a" → do
           pagem $ wlink "b" << contentFor "a"
           pagem $ wlink "c" << contentFor "b"
           pagem $ wlink "d" << contentFor "c"

  pagem $ wlink ()  << p << "back to the first page"

menu1 =  wcached "menu" 0 $ wlink "a" << b << "letters " <++ i << "or "   <|> wlink "1" << b << "numbers"

pagem  pagecode =page $ retry menu1 **> pagecode
     
contentFor x= do
        p << "page for"
        b << x
        p << "goto next page"

header1= html . body

With this code, every page has the menu on the top (letters or numbers). At any page the user can change from letters to numbers by clicking the menu.


No comments: