Web Objects and the underappreciated recursive do

I am doing some additional experiments with Haskell and web development. In this post, I am looking at the problem of composability for web pages.

I have tested different methods for building a web page and what I don't like is the lack of composability. When I look at most web pages, I see several components with their own state and control flow. But, when I build a web page from those components, the composability on the server side (for the request handlers) is less obvious.

So, I have attempted to build a composable solution based upon the idea of a web object. A web object is something with a state, that can be displayed on a web page and can react to some actions. Web objects must be reusable.

As an example, I have developed two web objects : a counter and a very simplified form with validation. The main function for the counter is:

instance WebObject Counter where
  render (Counter x) = do
     p $ do 
       text $ ("Counter value : " ++ (show x))
       actionLink "++"  (incrCount)
       actionLink "--"  (decrCount)
       actionLink "Go to Google" (goto "http://www.google.com")
       actionLink "Generate an output"  (myOutput)
       actionLink "Generate an error"  (myError)

A counter is just displaying the value of the counter and a few links. What is interesting is that a link is just some Haskell code. But, it is not a continuation. An action is a specific monadic value. The goal of this approach is to more easily organize the actions and connect them to some links on the page side. The state management and control flow is less important now that most web interfaces are using AJAX. But, even with AJAX, a web object needs to describe the set of actions that can be processed on the server and the links which are activating those actions.

Here is an example of action:

incrCount :: MonadCGI m => ActionMonad Counter m CGIResult
incrCount  = do
    Counter x <- get
    put (Counter (x+1))

An action can generate an error, generate an output (redirect, html) or just update the state of the web object.

Now that the web object is defined, it is possible to reuse it several times in the same web page. The below example is also using a Form web object:

body $
      p ("Test unicode : ")
      p ("éèçàùëï")
      webObject (initObject :: Form MyForm)
      webObject (initObject :: Counter)
      webObject (initObject :: Form MyForm)
      webObject (initObject :: Counter)

When a link is clicked, the right object will be updated and the other ones won't be changed. A web object is a totally independent part of the page with its own life. Web objects can be easily composed to build more complex pages.

So, how is it implemented. The idea is simple : when I receive an HTTP request, I must first compute the available actions to know how to interpret the request. To compute the available actions, which are spread everywhere in the definition of the web page, I must partially evaluate the monadic web value (I am working in a Web monad).

Once I have identified the actions available on a page, I can finish the evaluation of the value. So, it is an example of circular programming made possible by the lazy evaluation.

I could write it like that in a CGI monad:

 let (result,availableActions) = 
   runWeb webPage httpRequest availableActions
 output result

But, webPage is a monadic value so I need to use the recursive do and my Web monad must be instance of MonadFix. But, once done, it works well and you can test it here.

It is working as fastCGI. But, I am limited on the number of processes that I can launch since I am on a shared server. So, it may fail if there are too many connections. It is not a problem with Haskell.

If you enter ERROR or ERROR2 in the text field, you'll get some errors displayed before the form. The code is generic and using just a little bit of template Haskell for the Form.

I am not yet releasing the code because I'll have a lot of cleaning to do. But, I'll release it someday. I just wanted to share my enthusiasm :-)

Happy New Year !

(This post was imported from my old blog. The date is the date of the import. The comments where not imported.)

blog comments powered by Disqus