Read You A Blaze
This is a continuation of the ’Read You A _’™ series. In this series we will learn to create a toy version blaze-html
library. After reading this article, the reader should be able to easily read through the blaze-markup
and blaze-html
libraries. In addition the reader can gain good insight in building EDSL support in their projects.
HTML Blaze
blaze-html
is a Haskell library that provides a simple EDSL that can be used to build HTML pages in Haskell. This approach gives good type level guarantees while building HTML pages while sharing Haskell data structures.
To understand this tutorial, I suggest you familiarize yourself with some basic usage of this library. You should be able to follow this tutorial just with very basic understanding of how this library can be used.
Layout of this article:
Objective
The objective of ’Read You A _’™ series article is to understand how to build libraries in Haskell by reading and learning from some well known Haskell libraries. With that in mind, our objective for this tutorial would be to implement a toy version of the blaze-markup
library along with blaze-html
.
In this tutorial, we will not implement all the features of this library. But, we will end up in a position which will easily allow us to extend our toy example to include the other missing features.
Our goal for this tutorial is to be able to support the following snippet of code that is provided as a first example.
Code snippet that our final implementation will support
{-# LANGUAGE OverloadedStrings #-}
import Prelude hiding (head, id, div)
import Text.Blaze.Html4.Strict hiding (map)
import Text.Blaze.Html4.Strict.Attributes hiding (title)
import Text.Blaze.Renderer.Utf8 (renderMarkup)
page1 :: Markup
= html $ do
page1 head $ do
"Introduction page."
title ! rel "stylesheet" ! type "text/css" ! href "screen.css"
link $ do
body div ! id "header" $ "Syntax"
"This is an example of BlazeMarkup syntax."
p $ mapM_ (li . toMarkup . show) [1, 2, 3]
ul
--- And to render, we will also implement the `renderMarkup` function.
renderMarkup page1
If you are in a hurry and just would like to play with the example yourselves, all code shown in this article is available at read-you-a-blaze-html. Each iteration, in this tutorial produces a independent haskell modules. The four modules this tutorial produces are available in this repository.
About the Code snippet
The code snippet shown about uses functions from blaze-html
which internally uses functions provided by the blaze-markup
library. In this tutorial, we will be implementing data structures that overlap with both these libraries. It will become clear towards the end regarding where this data structures reside.
Before, we start off with the toy example, lets review the code snippet closely and identify the parts that we need to build. We are focusing our attention on the page1
function. The page1
function returns a Markup
type, which is defined in (Text.Blaze.Internal)[https://hackage.haskell.org/package/blaze-markup-0.8.2.7/docs/Text-Blaze-Internal.html] as
data MarkupM a = ...
....
...
type Markup = MarkupM ()
type Html = MarkupM () -- as defined in blase-html
Note, there is a type synonym Html
in blaze-html
that is also defined as type Html = MarkupM ()
.
The MarkupM
type will be the algebraic data type we will eventually build.
Also notice that html
, head
, body
functions take a Monad
. All these functions accept the Markup
type and return a Markup
type. Therefore, what ever data structure we end up with, we will also have to make that data structure implement the Monad
interface. We will also eventually define the functions html
, head
, title
etc.
Eventually, we also want to implement a version of renderMarkup
to render the data structure we cook up. We will add an implementation for that function as we go.
As you notice, we are taking a top down approach here. We are going to start with specific functions/data types that support HTML to help us support this code snippet. Gradually, we will generalize our code to work with any markup and not just HTML.
First Iteration (first.hs)
In our first iteration, we are going to support a slightly modified version of page1
that will help us build some understanding on the basic data structures we need.
Notice that functions that are used in the original page1
function, roughly have the following types:
-- for attributes
type Attribute = _ <- Yet to be defined
href :: String -> Attribute
type :: String -> Attribute
id :: String -> Attribute
rel :: String -> Attribute
-- for functions working on `Markup`, we have
type Html = _ <- Yet to be defined
link :: Html
title :: String -> Html
head :: Html -> Html
div :: Html -> Html
p :: Html -> Html
ul :: Html -> Html
li :: Html -> Html
-- and some helper functions
!) = _ (
The types of the above listed function informs us that we need two types Html
and Attributes
. Rather than work with these helper function, lets implement a more concrete version of page1
and then we will abstract away parts of it to use the helper functions.
Here is the modified version of page1
we will support in our first iteration:
page1 :: Html
= let
page1 = Link ! rel "style.css" ! type_ "text/html"
link' = Title "Introduction page."
title' = Head (Append link' title')
head' = mult Div (id_ "header") $ (Content "Syntax")
div' = P $ Content "This is an example of Blazemarkup syntax."
p' = UL $ foldHtml $ (map (LI . toMarkup . show) [1, 2, 3])
ul' = Body (Append (Append div' p') ul')
body' = HTML $ Append head' body'
html' in
html'
Notice that we have replaced calls to helper function with some data constructors
which we are yet to define. Since, all helper function’s are returning Html
as the data structure, lets create one that fits that setup.
data Attribute = Attribute String String
And, for Markup, let’s define. (Since, we are dealing concretely with HTML at this stage, lets name the data type accordingly
data Html = HTML Html
| Head Html
| Body Html
| Link
| Title String
| HRef String
| Div Html
| UL Html
| LI Html
| P Html
| Append Html Html
| AddAttribute Attribute Html
| Content String
| Empty
deriving (Show)
type Markup = Html
Note, the Link
, Title
, HRef
, Content
and Empty
are Leaf
values. The other data constructors are recursive. Now, with this type, we can start filling in our place holder functions. This data structure covers all the values that are used in the first version of page1
function.
We will also add the following helper functions.
-- helper functions to build attributes
href :: String -> Attribute
= Attribute "href" value
href value
type_ :: String -> Attribute
= Attribute "type" value
type_ value
id :: String -> Attribute
id value = Attribute "id" value
rel :: String -> Attribute
= Attribute "rel=" value rel value
Other helpers
toMarkup :: String -> Html
= Content s
toMarkup s
(!) :: Html -> Attribute -> Html
!) html attr = AddAttribute attr html
(
-- We will replace this definition later. For now assume, it is a version of (!) that supports a slightly different type.
mult :: (Html -> Html) -> Attribute -> Html -> Html
= \html2 -> AddAttribute attr (html1 html2)
mult html1 attr
foldHtml :: [Html] -> Html
:xs) = Append x (foldHtml xs)
foldHtml (x= Empty foldHtml []
We will also need a function that implements renderMarkup
. The first iteration of that function will be as follows. The renderMarkup
function walks over the Html
type and constructs the HTML
string.
renderMarkup :: String -> Html -> String
HTML inner) =
renderMarkup attrs ("<html" ++ attrs ++ ">" ++ renderMarkup "" inner ++ "</html>"
Head inner) =
renderMarkup attrs ("<head" ++ attrs ++ ">" ++ renderMarkup "" inner ++ "</head>"
Body inner) =
renderMarkup attrs ("<body" ++ attrs ++ ">" ++ renderMarkup "" inner ++ "</body>"
P inner) =
renderMarkup attrs ("<p" ++ attrs ++ ">" ++ renderMarkup "" inner ++ "</p>"
Link = "<link" ++ attrs ++ ">"
renderMarkup attrs Title s) = "<title" ++ attrs ++ ">" ++ s ++ "</title>"
renderMarkup attrs (HRef content) = "<a" ++ attrs ++ ">" ++ content ++ "</a>"
renderMarkup attrs (Div content) =
renderMarkup attrs ("<div" ++ attrs ++ ">" ++ (renderMarkup "" content) ++ "</div>"
UL content) =
renderMarkup attrs ("<ul" ++ attrs ++ ">" ++ (renderMarkup "" content) ++ "</ul>"
LI content) =
renderMarkup attrs ("<li" ++ attrs ++ ">" ++ (renderMarkup "" content) ++ "</li>"
Append html1 html2) =
renderMarkup attrs (++ (renderMarkup attrs html2)
(renderMarkup attrs html1) AddAttribute (Attribute k v) html) = flip renderMarkup html $
renderMarkup attrs (" " ++ k ++ "\"" ++ v ++ "\""
Content content) = content
renderMarkup attrs (Empty = "" renderMarkup attrs
With the above definitions we are ready to test out our first iteration. The fully implemented example of our first iteration can be found here: first.hs
Running this should produce the following output:
<html><head><link rel="style.css"><title>Introduction page.</title></head><body><div id"header">Syntax</div><p>This is an example of Blazemarkup syntax.</p><ul><li>1</li><li>2</li><li>3</li></ul></body></html>
End of first iteration
We created a concrete version data structures that will help us construct a HTML string. Our page1
function uses these concrete data structures to build the HTML
structure which can be converted to a HTML
string using the renderMarkup
function.
But, page1
is very verbose compared to the original version. We will need to extend our definitions to improve on the page1
function.
Second Iteration (second.hs)
As we have seen in the first iteration, it would be nice to implement functionality to support the original page1
function. We need to construct some sort of an EDSL to support this functionality. We will just do that in this iteration.
We need to support the EDSL used in page1
. To add that support, we will provide Functor
, Applicative
and Monad
instances to the Html
data type. But, before we do that, we need to update our Html
type to accept a type variable.
data HtmlM a = Html (HtmlM a)
| Head (HtmlM a)
| Body (HtmlM a)
| Link a
| Title String a
| HRef String a
| Div (HtmlM a)
| UL (HtmlM a)
| LI (HtmlM a)
| P (HtmlM a)
| forall b. Append (HtmlM b) (HtmlM a)
| AddAttribute Attribute (HtmlM a)
| Content String a
| Empty a
-- type synonyms for convenience
type Html = HtmlM ()
type Markup = HtmlM ()
We also also build a function’s which walks this type and yields the embedded value:
-- Walk through the Html data structure to return the embedded value. Used in Functor, Applicative and Monad instances
htmlValue :: HtmlM a -> a
Html x ) = htmlValue x
htmlValue (Head x ) = htmlValue x
htmlValue (Link x ) = x
htmlValue (HRef _ x) = x
htmlValue (Div x) = htmlValue x
htmlValue (LI x) = htmlValue x
htmlValue (UL x) = htmlValue x
htmlValue (P x) = htmlValue x
htmlValue (Append _ x) = htmlValue x
htmlValue (AddAttribute _ x) = htmlValue x
htmlValue (Content _ x) = x
htmlValue (Empty x ) = x htmlValue (
We also have to accordingly update the version of renderMarkup
that we used in first.hs
as follows:
--- Same as the version in first.hs
renderMarkup :: String -> HtmlM a -> String
Html inner) =
renderMarkup attrs ("<html" ++ attrs ++ ">" ++ renderMarkup "" inner ++ "</html>"
Head inner) =
renderMarkup attrs ("<head" ++ attrs ++ ">" ++ renderMarkup "" inner ++ "</head>"
Body inner) =
renderMarkup attrs ("<body" ++ attrs ++ ">" ++ renderMarkup "" inner ++ "</body>"
P inner) =
renderMarkup attrs ("<p" ++ attrs ++ ">" ++ renderMarkup "" inner ++ "</p>"
Link _) = "<link" ++ attrs ++ ">"
renderMarkup attrs (Title s _ ) = "<title" ++ attrs ++ ">" ++ s ++ "</title>"
renderMarkup attrs (HRef content _) = "<a" ++ attrs ++ ">" ++ content ++ "</a>"
renderMarkup attrs (Div content) =
renderMarkup attrs ("<div" ++ attrs ++ ">" ++ (renderMarkup "" content) ++ "</div>"
UL content) =
renderMarkup attrs ("<ul" ++ attrs ++ ">" ++ (renderMarkup "" content) ++ "</ul>"
LI content) =
renderMarkup attrs ("<li" ++ attrs ++ ">" ++ (renderMarkup "" content) ++ "</li>"
Append html1 html2) =
renderMarkup attrs (++ (renderMarkup attrs html2)
(renderMarkup attrs html1) AddAttribute (Attribute k v) html) = flip renderMarkup html $
renderMarkup attrs (" " ++ k ++ "\"" ++ v ++ "\""
Content content _) = content
renderMarkup attrs (Empty _) = "" renderMarkup attrs (
Also, update the helper functions
toMarkup :: String -> Html
= Content s ()
toMarkup s
foldHtml :: [Html] -> Html
:xs) = Append x (foldHtml xs)
foldHtml (x= Empty () foldHtml []
Once we have made these updates we are ready to implement the instances.
-- Need to update type of Append with existential
instance Functor HtmlM where
fmap f x = Append x (Empty (f (htmlValue x)))
instance Applicative HtmlM where
pure x = Empty x
-- Why are we not discarding the first value?
*>) = Append
(<*>) x y = --Empty (htmlValue x (htmlValue y))
(Append (Append x y) (Empty (htmlValue x (htmlValue y)))
instance Monad HtmlM where
return = pure
>>= f = Append m (f (htmlValue m))
m >>) = Append (
Note that the Applicative
and Monad
instances here append
the Html
. For our page1
to work it is sufficient to implement the Monad
instance. But, we have provided the Applicative
instance for completeness.
And, finally, our improved version of page1
is as follows. Note, that within each do
we append
the HTML parts and provide those parts as arguments to Html
, Head
and Body
.
page1 :: Html
= Html $ do
page1 Head $ do
Link () ! rel "style.css" ! type_ "text/html"
Title "Introduction page." ()
Body $ do
Div (id "header") $ Content "Syntax" ()
mult P $ Content "This is an example of Blazemarkup syntax." ()
UL $ foldHtml $ (map (LI . toMarkup . show) [1, 2, 3])
End of Second Iteration
We are pretty close know to the original implementation of page1
. We just need to provide some more syntax sugar. We can replace calls to Content "Syntax" ()
with just "Syntax"
. We can do this by providing a IsString instance.
First, we add this import and extension
{-# LANGUAGE TypeSynonymInstances #-} -- needed for ToMarkup String
import GHC.Exts (IsString (..))
and then provide an IsString
instance for the HtmlM a
type as follows:
instance (a ~ ()) => IsString (HtmlM a) where
fromString x = Content (fromString x) mempty
Also, since HtmlM a
is a monad, we can replace the call for foldHtml
with the standard ‘mapM_’ function. The revised page1
after removing call to Content
and foldHTML
.
page1 :: Html
= Html $ do
page1 Head $ do
Link () ! rel "style.css" ! type_ "text/html"
Title "Introduction page." ()
Body $ do
Div (id "header") $ "Syntax"
mult P $ "This is an example of Blazemarkup syntax."
UL $ mapM_ (LI . toMarkup . show) [1, 2, 3]
Third Iteration (third.hs)
If we review the final version of page1
from the second iteration you will notice that the client/user directly is using the HtmlM a
data structure. We would like to abstract that away into some helper function. This gives us the flexibility of updating/extending the data structure without affecting the client code. In this iteration we will do just that. We will provide helper functions that the client can use to construct these HTML elements and attributes.
Here are the list of helper functions we need to define:
-- HTML helpers
html :: Html -> Html
= Html
html
head :: Html -> Html
head = Head
link :: Html
= Link ()
link
title :: String -> Html
= Title s ()
title s
div :: Html -> Html
div = Div
p :: Html -> Html
= P
p
ul :: Html -> Html
= UL
ul
li :: Html -> Html
= LI
li
body :: Html -> Html
= Body body
And now the client can use these helper functions rather than constructing the HTML elements using the HtmlM a
data type. There is another benefit for this abstraction that will become clear when we do our final fourth iteration.
After replacing the page1
function with the helper function, the new version of page
will be as follows:
page1 :: Html
= html $ do
page1 head $ do
! rel "style.css" ! type_ "text/html"
link "Introduction page."
title $ do
body Div (id "header") $ "Syntax"
mult $ "This is an example of Blazemarkup syntax."
p $ mapM_ (li . toMarkup . show) [1, 2, 3] ul
Almost close!
We just need to replace mult
with a (!)
call and we have achieved the objective we started with. Let’s wrap up this iteration with that change. To replace mul
with (!)
, we need to define a type class
that will support both HtmlM a
and HtmlM a -> HtmlM a
.
First add some language extensions
{-# LANGUAGE FlexibleInstances #-}
class Attributable h where
(!) :: h -> Attribute -> h
instance Attributable (Html) where
!) html attr = AddAttribute attr html
(
instance Attributable (Html -> Html ) where
! attr = \f1 -> AddAttribute attr $ f f1 f
Also, we remove our previous implementation of (!)
.
-- Remove this function after adding the above Attributable interfaces
(!) :: Html -> Attribute -> Html
!) html attr = AddAttribute attr html (
The first instance will support leaf level Html
elements such as Link
, Title
etc. The second instance is for elements such as Div
, P
etc. That is any element, that can embed more Html
elements.
With that change, we have our final page1
function which looks exactly like the one we start off with as our objective.
page1 :: Html
= html $ do
page1 head $ do
! rel "style.css" ! type_ "text/html"
link "Introduction page."
title $ do
body div ! (id "header") $ "Syntax"
$ "This is an example of Blazemarkup syntax."
p $ mapM_ (li . toMarkup . show) [1, 2, 3] ul
One Last Iteration (fourth.hs)
So, far we have seen that the algebraic data type HtmlM a
only works for Html. But, if you look closely the pattern we followed so far would work for a wider range of Markups. Html or other Markups have a tree like structure with Parent Nodes, Siblings and Leaf Nodes with attributes attaching themselves to these nodes. If we can transform our current HtmlM a
into a more generic structure, then we could extend our library to other markups that share similar structure. In this iteration, that is precisely what we will do. We will update the code to make it support a more general markup declaration and see how it works with the concrete HTML implementation.
As stated, if we view the Markup
syntax as a syntax to build a tree, then we can arrive at this type:
type Tag = String
type Open = String
type Close = String
type Key = String
type Value = String
data MarkupM a
= Parent Tag Open Close (MarkupM a)
| Leaf Tag Open Close a
| Content String a
| forall b. Append (MarkupM b) (MarkupM a)
| AddAttribute Attribute (MarkupM a)
| Empty a
type Markup = MarkupM ()
Note that leaf
elements like Link
, Href
, Content
in previous HtmlM a
declaration or just Leaf
here, and elements that support HTML embedding like Div
, P
can be represented as Parent
here. The values to the data constructor have been added to replicate the declaration in the (Text.Blaze.Internal
)[http://hackage.haskell.org/package/blaze-markup-0.8.2.1/docs/src/Text.Blaze.Internal.html#MarkupM]
With that it is time to update our htmlValue
function and replace it with the generic markupValue
function. This again is the same as the markupValue
function in (Text.Blaze.Internal
)[http://hackage.haskell.org/package/blaze-markup-0.8.2.1/docs/src/Text.Blaze.Internal.html]
Here are the updated definitions:
markupValue :: MarkupM a -> a
Parent _ _ _ x) = markupValue x
markupValue (Leaf _ _ _ x) = x
markupValue (Content _ x) = x
markupValue (Append _ m1) = markupValue m1
markupValue (AddAttribute _ m1) = markupValue m1
markupValue (Empty x) = x markupValue (
We also need to apply similar updates to the Functor
, Applicative
and Monad
instances, and also to IsString
and Attributable
instances.
-- Need to update type of Append with existential
instance Functor MarkupM where
fmap f x = Append x (Empty (f (markupValue x)))
-- Why isn't this just an `(Empty (htmlValue x (htmlValue y)))`
-- The `homomorphism` law is not followed here.
instance Applicative MarkupM where
pure x = Empty x
-- Why are we not discarding the first value?
*>) = Append
(<*>) x y = --Empty (htmlValue x (htmlValue y))
(Append (Append x y) (Empty (markupValue x (markupValue y)))
instance Monad MarkupM where
return = pure
>>= f = Append m (f (markupValue m))
m >>) = Append
(
-- other instances
instance (a ~ ()) => IsString (MarkupM a) where
= Content (fromString x) mempty
fromString x
class Attributable h where
(!) :: h -> Attribute -> h
instance Attributable Markup where
!) html attr = AddAttribute attr html
(
instance Attributable (Markup -> Markup) where
! attr = \f1 -> AddAttribute attr $ f f1
f
class ToMarkup a where
toMarkup :: a -> Markup
instance ToMarkup String where
= string toMarkup
Finally, if you remember, in our previous iteration, we abstracted away the creation of HTML
elements into small helper functions. That move come in handy here, to construct concrete values for the new Markup
type. You can observe that in the updates that we make to the helper functions as follows. This is the part that is made available to the client from the blaze-html
library, whereas the code we saw above is abstracted away int blaze-markup
library.
= Attribute "href=\"" value
href value = Attribute "type=\"" value
type_ value
= Attribute "rel=\"" value
rel value
id value = Attribute "id=\"" value
type Html = MarkupM ()
html :: Html -> Html
= Parent "html" "<html" "</head>"
html
head :: Html -> Html
head = Parent "head" "<head" "</head>"
title :: Html -> Html
= Parent "title" "<title" "</title>"
title
link :: Html
= Leaf "link" "<link" ">" ()
link
body :: Html -> Html
= Parent "body" "<body" "</body>"
body
div :: Html -> Html
div = Parent "div" "<div" "</div>"
p :: Html -> Html
= Parent "p" "<p" "</p>"
p
ul :: Html -> Html
= Parent "ul" "<ul" "</ul>"
ul
li :: Html -> Html
= Parent "li" "<li" "</li>"
li
string :: String -> Html
= Content a () string a
Note that we are constructing values from the Markup
type and providing the ‘string’ rendered version of Html
elements. This would mean we also update our renderMarkup
version to support this version. This function is also available in the generic markup library!
renderString :: String -> MarkupM a -> String
Parent tag open close content) =
renderString attrs (++ attrs ++ ">" ++ (renderString "" content) ++ close
open Leaf _ begin end _) = begin ++ attrs ++ end
renderString attrs (Content x _) = x
renderString attrs (Append m1 m2) =
renderString attrs (++ renderString attrs m2
renderString attrs m1 AddAttribute (Attribute key value) m1) =
renderString attrs (flip renderString m1 $
" " ++ key ++ value ++ "\"" ++ attrs
Empty x) = "" renderString attrs (
Finally, our version of page1
from previous iteration will continue to work.
page1 :: Html
= html
page1 $ do
head $ do
"Introduction page."
title ! rel "stylesheet" ! type_ "text/css" ! href "screen.css"
link $ do
body div ! id "header" $ "Syntax"
"This is an example of BlazeMarkup syntax."
p $ mapM_ (li . toMarkup . show) [1, 2, 3] ul
Here is the final version of the code that will support the original version of page1
function. fourth.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE TypeSynonymInstances #-} -- needed for ToMarkup String
{-# LANGUAGE FlexibleInstances #-}
--module Main1 where
import Prelude hiding (head, id, div)
import GHC.Exts (IsString (..))
data Attribute = Attribute String String
deriving (Show)
type Tag = String
type Open = String
type Close = String
type Key = String
type Value = String
data MarkupM a
= Parent Tag Open Close (MarkupM a)
| Leaf Tag Open Close a
| Content String a
| forall b. Append (MarkupM b) (MarkupM a)
| AddAttribute Attribute (MarkupM a)
| Empty a
instance (a ~ ()) => IsString (MarkupM a) where
= Content (fromString x) mempty
fromString x
markupValue :: MarkupM a -> a
Parent _ _ _ x) = markupValue x
markupValue (Leaf _ _ _ x) = x
markupValue (Content _ x) = x
markupValue (Append _ m1) = markupValue m1
markupValue (AddAttribute _ m1) = markupValue m1
markupValue (Empty x) = x
markupValue (
type Markup = MarkupM ()
-- Need to update type of Append with existential
instance Functor MarkupM where
fmap f x = Append x (Empty (f (markupValue x)))
-- Why isn't this just an `(Empty (htmlValue x (htmlValue y)))`
-- The `homomorphism` law is not followed here.
instance Applicative MarkupM where
pure x = Empty x
-- Why are we not discarding the first value?
*>) = Append
(<*>) x y = --Empty (htmlValue x (htmlValue y))
(Append (Append x y) (Empty (markupValue x (markupValue y)))
instance Monad MarkupM where
return = pure
>>= f = Append m (f (markupValue m))
m >>) = Append
(
renderString :: String -> MarkupM a -> String
Parent tag open close content) =
renderString attrs (++ attrs ++ ">" ++ (renderString "" content) ++ close
open Leaf _ begin end _) = begin ++ attrs ++ end
renderString attrs (Content x _) = x
renderString attrs (Append m1 m2) =
renderString attrs (++ renderString attrs m2
renderString attrs m1 AddAttribute (Attribute key value) m1) =
renderString attrs (flip renderString m1 $
" " ++ key ++ value ++ "\"" ++ attrs
Empty x) = ""
renderString attrs (
= Attribute "href=\"" value
href value = Attribute "type=\"" value
type_ value
= Attribute "rel=\"" value
rel value
id value = Attribute "id=\"" value
class Attributable h where
(!) :: h -> Attribute -> h
instance Attributable Markup where
!) html attr = AddAttribute attr html
(
instance Attributable (Markup -> Markup) where
! attr = \f1 -> AddAttribute attr $ f f1
f --f ! attr = AddAttribute attr $ f
type Html = MarkupM ()
html :: Html -> Html
= Parent "html" "<html" "</head>"
html
head :: Html -> Html
head = Parent "head" "<head" "</head>"
title :: Html -> Html
= Parent "title" "<title" "</title>"
title
link :: Html
= Leaf "link" "<link" ">" ()
link
body :: Html -> Html
= Parent "body" "<body" "</body>"
body
div :: Html -> Html
div = Parent "div" "<div" "</div>"
p :: Html -> Html
= Parent "p" "<p" "</p>"
p
ul :: Html -> Html
= Parent "ul" "<ul" "</ul>"
ul
li :: Html -> Html
= Parent "li" "<li" "</li>"
li
string :: String -> Html
= Content a ()
string a
class ToMarkup a where
toMarkup :: a -> Markup
instance ToMarkup String where
= string
toMarkup -- Implement escaping
--preEscapedToMarkup = preEscapedString
page1 :: Html
= html
page1 $ do
head $ do
"Introduction page."
title ! rel "stylesheet" ! type_ "text/css" ! href "screen.css"
link $ do
body div ! id "header" $ "Syntax"
"This is an example of BlazeMarkup syntax."
p $ mapM_ (li . toMarkup . show) [1, 2, 3]
ul
main :: IO ()
= putStrLn $ renderString "" page1 main
Conclusion
If you have stayed with me so far, Congrats! We just implemented a simple version of EDSL which allows clients to build HTML
contents using Haskell syntax. In the process we learned how to define the abstract data types that defined the EDSL and also provide a friendly monadic
interfaces. In this process, we also identified the parts of code that are currently available as part of the two libraries, namely, blaze-markup
and blaze-html
.
There are other important pieces in the blaze-html
and blaze-markup
libraries that this article does not touch upon. That is using efficient String
types by way of a ChoiceString
data type. But, that information and other missed pieces of the implementations should be easy to understand by reading the source code of these libraries.
Hopefully, the insights you gained building this toy example, will help you read and understand the internals of these 2 libraries easily. This should also help you build a good foundation based on which you would be able to create your own EDSL’s. Happy Haskell hacking!
See you in the next ‘Read Your A _ Series’™.
References
Special thanks to jaspervdj and other contributors for creating such a nice set of libraries.