Friday, June 06, 2014

Taming the HTML DOM with monads and monoids

Haste is a compiler that generates Javascript code from Haskell.
The Haste.DOM module define a thin layer over the JavaScript DOM. That makes the creation and manipulation of DOM elements as painful as in JavaScript. The reason is because to add an element it is necessary two steps: to create the element and get the reference to that elemen, and to append the element as child of the parent.  This linking of references by hand is what makes the creation of dynamic HTML hard, tedious and inelegant.
That is why all the Haskell-Javascript compilers have static HTML demos most of the time, and they concentrate in the creation of graphics, that CAN be programmed in a more pleasant way.
This package makes the creation of DOM elements easy with a syntax similar to other haskell HTML generators, using monoids and monads, such is the case of the package blaze-html.
This is an example. withElem is a Haste.DOM call that give the DOM object whose id is "idelem", that has been created "by hand" in Main.hs. The builder that I created (see the link below) takes this element and add content by defining a hierarchy within the `JSBuilder` monad:
  main= do
   withElem "idelem" . build $ do
    div $ do
       div  $ do
           p "hello"
           nelem "p" `attr` ("style","color:red")  `child`  "world" 
    return ()

   div cont=  nelem "div" `child`  cont

   p cont = nelem "p"  `child`  cont
`child` and `nelem` are defined in the builder too.

No element references have to be managed by the programmer unlike in the case of the plain DOM interface. Try to do it using plain DOM calls.

The output in the browser is:
NOTE: blogger is sooo ugly . Execute it and look at the HTML code for yourself.
The HTML rendering is:
hello
world
How it works? The basic element is a  `builder ` data type that has a "hole" parameter and a IO action which creates the element. The hole contains the parent (Elem) of the element being created.

newtype JSBuilderM a= JSBuilder{build :: Elem -> IO Elem} deriving Typeable
type JSBuilder = JSBuilderM ()
The phanton type 'a' is in order make a valid monad instance of JSBuilderM with the appropriate kind. Nothing more.

Upon created, the elem is added to the parent and return itself as parent of the next elements. That is the creation of an element:

nelem s= JSBuilder $ \e ->do
    e' <- newElem s
    addChild e' e
    return e'

To append two elements,  it executes the build-link procedure defined for each element to the parent, and return the parent node:

instance Monoid (JSBuilderM a) where
    mappend mx my= JSBuilder $ \e -> do
         build mx e
         build my e
         return e
    mempty = JSBuilder return
The expression:

build mx e

Executes the IO computation for the creation of the element/elements included (it may be a sub-tree). 

To add a child, the parent's computation is executed, the chid is converted into a builder (using a ToElem instance) and the builder is executed taking the parent as parameter. Finally the parent is returned


child :: ToElem a => JSBuilder -> a -> JSBuilder
child me ch= JSBuilder $ \e' -> do
        e <- build me e'
        let t = toElem ch
        .build t e
        return e

Similarly, to add an attribute to an elem:


attr tag (n, v)=JSBuilder $ \e -> do
        tag' $lt;- build tag e
        setAttr tag' n v
        return tag'

The Monad instance is there in order to use the do notation.  This add a new level of syntax, in the style of the package blaze-html. This monad invokes the same appending mechanism.
This makes the creation of dynamic Web apps in the browser with texts and formatting far more easy, as a seamless declarative sequence with the shape of the DOM three being created, rather than as a imperative sequence full of seams.
The equivalent monoid expression can also be used, by concatenating elements with the operator <> or mappend

I guess this technique can be generalized for the creation of any tree data structure and any kind of tree management primitives.
The code and how it works, the demo etc is here:
https://github.com/agocorona/haskell-js-html-builder

UPDATE:
this software is now called "perch" and is published in the hackage repository. It support now the same syntax than blaze-html including attribute (!) operators etc.

https://hackage.haskell.org/package/haste-perch

Post a Comment