404 No More!

(Part II and Part III)

This post shows a simple way to use the Reader Monad Transformer to:

  1. Ensure that all the internal links in your web app are valid.
  2. Ensure that when a component is used multiple times, each instance gets a unique set of URLs.

The core code is mind-numbingly simple -- one type signature and two one-liner functions. In fact, it is so simple looking, it will take me three parts to explain why it is actually somewhat cool and useful. Some familiarity with the Reader monad is useful but not essential.

I expect that the technique shown in this series is applicable to other problems, but I am focusing on hyperlinks, because that is what is immediately useful to me. If you think up some other pratical uses, let me know and I will mention them.

First some header stuff. > {-# LANGUAGE DeriveDataTypeable, FlexibleContexts #-} > module Main where > import Control.Concurrent > import Control.Monad.Trans > import HAppS.Server hiding (method, dir) > import Text.XHtml > import Network.URI

An Obvious Start

The first step is to present the links as data types in the code. For example, let's imagine we have a simple image gallery. The gallery has two views:

  1. View a thumbnail gallery of all the images
  2. View an individual image

Furthermore, when viewing an invidual image, we might view it:

  1. Fullsize
  2. Scaled down to screen size
We can represent navigating to those pages with the following types: > data Gallery > = Thumbnails > | ShowImage Int Size > deriving (Read, Show) > data Size > = Full > | Screen > deriving (Read, Show)

Note that the data type only specifies which page to view. It does not include all of the information about the page, such as the filepath to the image, the image size etc. This data type only includes the type of information normally found in a URL.

Next we need some functions to turn our type into a Link and back. Here we will make Link be a simple String, but it could be something fancier, such as Network.URI if desired.

For the first pass, we will just use show and read with some extra functions to encode and decode special characters which can not appear in a URI. Later we will see how to implement showLink and readLink so that they generate prettier and more user friendly links.

> type Link = String > showLink :: (Show a) => a -> Link > showLink = escapeURIString isUnescapedInURI . show > readLink :: (Read a) => Link -> Maybe a > readLink = readM . unEscapeString > where > readM :: (Read a) => String -> Maybe a > readM str = > case reads str of > [(a,"")] -> Just a > _ -> Nothing

Next we implement a function which will interpret the Gallery link and display the corresponding page:

> -- dummy implementation for didactic purposes > gallery :: String -> Gallery -> Html > gallery username Thumbnails = > let img1 = showLink (ShowImage 1 Full) > in pageTemplate > ((toHtml $ "Showing " ++ username ++ "'s gallery thumbnails.") +++ > br +++ > (anchor (toHtml "image 1") ! [href img1])) > gallery username (ShowImage i s) = > pageTemplate (toHtml $ "showing " ++ username ++ "'s image number " ++ > show i ++ " at " ++ show s ++ " size.") > pageTemplate :: Html -> Html > pageTemplate thebody = > ((header > (thetitle (toHtml "Simple Site"))) +++ > (body thebody))


The Good

There are two good things to note about gallery.

  1. Looking up the page associated with the incoming link uses standard Haskell pattern matching. Therefore, the compiler can warn use if we have incomplete pattern matches.
  2. In the code, the links are represented as datatypes, not Strings. This means the compiler will catch typos, mismatched types, missing arguments, and other similar errors at compile time.

The Bad

We have eliminated some causes of invalid links, but there are still many uncaught errors lurking around. For example, if we replace, showLink (ShowImage 1 Full) with showLink True, the code will still compile without any errors. But, at runtime, when you clicked on the link, it will try to find a match for True and get a 404. Additionally, if we try to use the Gallery library in two places in a larger site, the generated URIs will be wrong and non-unique. For example, imagine if we had a site like: > data OurSite > = HomePage > | MyGallery Gallery > | YourGallery Gallery > deriving (Read, Show) In the next post we will see how to Iaddress these two issues.

The Rest Of This Example

The remaining code is just some boilerplate code for calling our gallery example. It uses HAppS, but could easily be adapted to use Network.CGI. > data Site link a > = Site { handleLink :: link -> a > , defaultPage :: link > } > runSite :: (Read link) => Site link a -> Link -> Maybe a > runSite site linkStr = > let mLink = > case linkStr of > "" -> Just (defaultPage site) > _ -> readLink linkStr > in > fmap (handleLink site) mLink > simpleSite :: Site Gallery Html > simpleSite = > Site { handleLink = gallery "Jeremy" > , defaultPage = Thumbnails > } > -- * Boilerplate code for running simpleSite via HAppS. > -- Easily adaptable to Network.CGI, etc. > implURL :: [ServerPartT IO Response] > implURL = > [ withRequest $ \rq -> > let link = (concat (take 1 (rqPaths rq))) > in > do lift $ print link > return . toResponse $ runSite simpleSite link > ] > main :: IO () > main = > do tid <- forkIO $ simpleHTTP nullConf implURL > putStrLn "running..." > waitForTermination > killThread tid