Introduction
In Haskell you can use the Regular generic programming library to create links to- and handle URLs that are guaranteed to work if your code compiles. This means, no more dead links - if you modify the route, the compiler will issue errors if you forgot to update a link somewhere in your application.
Generic programming is a technique that allows writing functions that work for all values, in this case - a URL datatype called BlogURL that models the route to the original request. The example below uses the Happstack web server library. After running the example, open http://localhost:8000/ to access the application.
Requirements for this tutorial
Some minimal knowledge of Haskell and Monads is assumed. Additionally, you will need the following hackage dependencies to get the code to run:
- blaze-html >= 0.2.3: Although we used the new blaze-html in this example, it can be replaced by basically any library that can output HTML content.
- web-routes: The library that does the routing.
- web-routes-happstack: Some 'glue' code to make web routes work on happstack
- web-routes-regular: Allows type safe URLs by deriving routes from URL datatypes.
- regular >= 0.2.4: Generic programming library needed by web-routes-regular.
- utf8-string
- bytestring
- happstack-server: Finally, the server itself.
Source code
First some import statements and language pragma:
{-# LANGUAGE OverloadedStrings, TemplateHaskell, EmptyDataDecls,
TypeFamilies, TypeSynonymInstances #-}
module Main where
import Happstack.Server (simpleHTTP, ToMessage(..), toContentType,
toMessage, ok, nullConf, ServerPartT)
import Web.Routes.Site (Site (..), setDefault)
import Web.Routes.PathInfo (PathInfo (..), parseSegments)
import Web.Routes.Happstack (implSite)
import Web.Routes.Regular (gtoPathSegments, gfromPathSegments)
import Web.Routes.RouteT (RouteT(..), runRouteT, showURL)
import Generics.Regular (deriveAll)
import Generics.Regular.Base (PF (..), from, to)
import Text.Blaze.Renderer.String
import Text.Blaze.Html5
import Text.Blaze.Html5.Attributes
import qualified Text.Blaze.Html5 as H
import qualified Text.Blaze.Html5.Attributes as A
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.UTF8 as LU (toString, fromString)
Next, we specify that the Blaze HTML datatype (H.Html) can be converted to a response. The ToMessage Happstack class requires to provide a content type that will be used in the response, as well as a way of converting the value (in this case of type H.Html) to a String.
instance ToMessage H.Html where
toContentType _ = B.pack "text/html; charset=UTF-8"
toMessage = LU.fromString . renderHtml
Moving onwards, we define the datatype that will represent URLs. All constructors of this datatype will become website entry points. In this example, you can access urls such as http://localhost:8000/index or http://localhost:8000/post/3, which correspond to the Index and Post constructors. What follows after the data definition is part of the Regular library book-keeping, that will be explained perhaps in another tutorial. It's possible to use nested (but not mutually-recursive) datatypes as routes.
data BlogURL = Index
| Post Int
$(deriveAll ''BlogURL "PFBlogURL")
type instance PF BlogURL = PFBlogURL
instance PathInfo BlogURL where
toPathSegments = gtoPathSegments . from
fromPathSegments = fmap to gfromPathSegments
And that's it! That's basically all we have to do to define the routes. The rest of the code is used to glue route handling to Happstack, and also to help create links to pages.
To do that, we define two monadic type aliases: BlogServer is the server monad, and RoutedBlogServer applies the RouteT transformer on top of it. RouteT is needed for constructing the links, using the showURL function (that we will see below) and other functions. Route handling (i.e. the web-routes package) works by defining a Site value containing the following definitions:
- handleSite: A function that takes a URL building function and a BlogURL value as parameters and returns a corresponding BlogServer.
- formatPathSegments: Takes a BlogURL and returns a list of strings (path segments).
- parsePathSegments: A function that takes a list of path segment strings and returns either a corresponding BlogURL value (if parsing succeeds) or an error message. In our case, we get these two last functions for free from the regular web library. If this were not so, we could have defined more customized URL handlers.
type BlogServer = ServerPartT IO
type RoutedBlogServer = RouteT BlogURL BlogServer
blogSite :: Site BlogURL (BlogServer Html)
blogSite = setDefault Index Site {
handleSite = blogHandle
, formatPathSegments = toPathSegments
, parsePathSegments = parseSegments fromPathSegments
}
Now as for the URL handling function (blogHandle), we call the runRouteT transformer function, to which we pass the URL building function. The RouteT monad will use this function to return the appropriate links, that are built using it and the formatPathSegments parameter from the Site structure.
The blogHandle function calls routedBlogHandle, which is like this taken out of the RoutedBlogServer and into the BlogServer monad. This is where the neat part comes: we can interpret the request by pattern matching on BlogURL values. And we can also build links to be displayed in the page (as strings) by using showURL, as follows:
blogHandle :: (BlogURL -> String) -> BlogURL -> BlogServer Html
blogHandle f url = runRouteT (routedBlogHandle url) f
routedBlogHandle :: BlogURL -> RoutedBlogServer Html
routedBlogHandle (Post idPost) = do
indexLink <- showURL Index
nextPostLink <- showURL (Post $ idPost + 1)
ok $ do
p $ "Showing blog post number:"
p $ string (show idPost)
p $ a ! href (stringValue indexLink) $ "Go to Homepage"
p $ a ! href (stringValue nextPostLink) $ "Go to the next post"
routedBlogHandle (Index) = do
firstPostLink <- showURL (Post 1)
ok $ do
p $ "Homepage"
p $ a ! href (stringValue firstPostLink) $ "Go to the first post"
Finally, in the main function, some code to let the web-routes take care of routing and to start the HTTP server:
main :: IO ()
main = do
let server = implSite "http://localhost:8000/" "" blogSite
simpleHTTP nullConf server
Conclusion
We have shown how to use a regular programming library and web-routes to handle URLs in a type-safe way. Many improvements would be needed to have a full-fledged web application, including database connectivity and session handling, but hopefully this documentation has been helpful. Have suggestions for improvement? Feel free to edit it and start a new discussion!