Web Application Programming in Haskell

Introduction

This tutorial aims to get you started with writing web applications in Haskell. We describe a relatively light-weight approach to Haskell web programming which uses a CGI library and an XHTML combinator library.

There are several other approaches, some of which are mentioned below. We think that while the approach we describe here is not as sophisticated or innovative as some of the others, it is simple, portable and easy to understand.

The tutorial starts with preliminaries such as how to install the necessary software and how to compile and run your web applications. We then show a number of working small example programs which introduce the basic features of the CGI and XHtml libraries. We then move on how to use monad transformers to added application specific functionality such as sessions to the CGI monad, and how to create database-driven web applications.

Other approaches

HSP (Haskell Server Pages) "Haskell Server Pages (HSP) is an extension of vanilla Haskell, targetted at the task of writing dynamic server-side web pages."

WASH (Web Authoring System Haskell) "WASH is a family of embedded domain specific languages (EDSL) for programming Web applications. Each language is embedded in the functional language Haskell, which means that it is implemented as a combinator library."

HAppS (Haskell Application Server) "HAppS is a Haskell library for building industrial strength Internet applications safely, quickly, and easily. With HAppS you focus entirely on application functionality implemented in your favorite language and you don't have to worry about making sure all sorts of server subsystems are functioning properly."

Assumed knowledge

This tutorial is not meant as an introduction to Haskell or web programming. We will assume that you have some familiarity with the following concepts:

Haskell

This tutorial is not meant as a first introduction to Haskell. If you want to learn about Haskell in general, have a look at the tutorials listed on the Haskell homepage. Haskell in 5 steps page might be a good place to start.

HTML

HTML (HyperText Markup Language) is the "the lingua franca for publishing hypertext on the World Wide Web". The XHtml library which we use in this tutorial produces XHTML 1.0, which is HTML 4.0 formulated as XML.

The combinators in the XHtml library do not make much sense unless you understand at least some parts of HTML.

CGI

To really understand how the CGI library works, you probably need to know a thing or two about CGI. CGI (Common Gateway Interface) is the protocol which your web application uses to communicate with the web server.

Required software

GHC

GHC, the Glasgow Haskell Compiler, is the Haskell implementation that we will use in this tutorial. If you do not already have it installed, you need to download GHC and see the GHC User's Guide for how to install it on your system.

xhtml package

You will need to download and install the xhtml package.

cgi package

You will also need to download and install the cgi package.

Web server

You need to have access to a web server on which you can run CGI programs. The most convenient way to do this when learning and developing is to run a web server on your development machine. If you run the programs on some other machine you need to make sure that you compile your programs so that they can run on that machine. This normally means that the machines must to have the same architecture and run the same operating system. Linking your applications statically by giving the flags ''-static -optl-static'' to GHC will avoid problems with missing libraries on the web server.

Compiling and running web applications

Compile with GHC:

ghc -package cgi -package xhtml -fallow-overlapping-instances --make -o prog.cgi prog.hs

Put the compiled program in the cgi-bin directory, or give it the extension .cgi, depending on the configuration of the web server.

Visit the URL of the CGI program with your web browser.

Basic web applications

The examples aren't something you'd actually use, but they illustrate some things that you can do.

Hello World

Here is a very simple example which just outputs some static HTML.

The type signatures in this code are optional. We show them here for clarity, but omit them in some later examples.

import Network.NewCGI
import Text.XHtml

page :: Html 
page = body << h1 << "Hello World!"

cgiMain :: CGI CGIResult
cgiMain = output $ renderHtml page

main :: IO ()
main = runCGI $ handleErrors cgiMain

hello.hs

Live demo

Html is the type of HTML fragments. It comes from the Text.XHtml module. See section ??? for more on creating HTML.

renderHtml :: Html -> String creates a string containg the HTML document.

output :: String -> CGI CGIResult outputs a string as the body of the HTTP response.

handleErrors :: CGI CGIResult -> CGI CGIResult Catches any exception thrown by the given CGI action, returns an error page with a 500 Internal Server Error, showing the exception information, and logs the error.

runCGI :: CGI CGIResult -> IO () runs a CGI action which produces a CGIResult, using the CGI protocol to get the inputs and send the outputs.

HTML combinators

There are functions for all XHTML 1.0 elements. Some examples:

The << operator is used for nesting HTML.

Attributes are added to tags using the ! operator.

The function renderHtml (explain variants) produces a string containing the document.

Getting user input

This program shows a form which asks the user for her name. When the form is submitted, the program greets the user by name.

import Network.NewCGI
import Text.XHtml

inputForm = form << [paragraph << ("My name is " +++ textfield "name"),
                     submit "" "Submit"]

greet n = paragraph << ("Hello " ++ n ++ "!")

page t b = header << thetitle << t +++ body << b

cgiMain = do mn <- getInput "name"
             let x = maybe inputForm greet mn
             output $ renderHtml $ page "Input example" x

main = runCGI $ handleErrors cgiMain

input.hs

Live demo

getInput :: String -> CGI (Maybe String) Get the value of an input variable, for example from a form. If the variable has multiple values, the first one is returned.

Cookies

import Network.NewCGI
import Text.XHtml

import Control.Monad (liftM)
import Data.Maybe (fromMaybe)

hello :: Int -> Html
hello 0 = h1 << "Welcome!"
          +++ p << "This is the first time I see you."
hello c = h1 << "Welcome back!"
          +++ p << ("I have seen you " ++ show c ++ " times before.")

page :: String -> Html -> Html
page t b = header << thetitle << t +++ body << b

cgiMain :: CGI CGIResult
cgiMain = do c <- liftM (fromMaybe 0) $ readCookie "mycookie"
             setCookie (newCookie "mycookie" (show (c+1)))
             output $ renderHtml $ page "Cookie example" $ hello c

main :: IO ()
main = runCGI $ handleErrors cgiMain

cookie.hs

Live demo

Here we use newCookie, setCookie and readCookie to store and retrieve a counter cookie in the browser.

If you want to get the string value of a cookie, use getCookie instead of readCookie.

File uploads

-- Accepts file uploads and saves the files in the given directory.
-- WARNING: this script is a SECURITY RISK and only for 
-- demo purposes. Do not put it on a public web server.

import Network.NewCGI
import Text.XHtml

import qualified Data.ByteString.Lazy as BS

import Control.Monad (liftM)
import Data.Maybe (fromJust)

dir = "../upload"

fileForm = form ! [method "post", enctype "multipart/form-data"]
             << [afile "file", submit "" "Upload"]
saveFile n =
    do cont <- liftM fromJust $ getInputFPS "file"
       let f = dir ++ "/" ++ basename n
       liftIO $ BS.writeFile f cont
       return $ paragraph << ("Saved as " +++ anchor ! [href f] << f +++ ".")

page t b = header << thetitle << t +++ body << b

basename = reverse . takeWhile (`notElem` "/\\") . reverse

cgiMain = 
    do mn <- getInputFilename "file"
       h <- maybe (return fileForm) saveFile mn
       output $ renderHtml $ page "Upload example" h

main = runCGI $ handleErrors cgiMain

upload.hs

We first output a file upload form, which should use the HTTP POST method, and the multipart/form-data content type. Here we seen an example of the use of HTML attributes, added with the ! operator.

For efficiency reasons, we use Data.ByteString.Lazy to represent the file contents. getInputFPS gets the value of an input variable as a lazy ByteString.

Setting response headers

You can use the setHeader function to set arbitrary HTTP response headers.

Returning non-HTML

Of course we do not have to output HTML. Use setHeader to set the value of the Content-type header, and you can output whatever string you like.

RSS

If you want to create an RSS feed in you application, you can use the Haskell rss module.

Error handling

handleErrors catches all exceptions and outputs a default error page with some information about the exception. You can write you own exception handler if you want to do something else when an exception is thrown.

MonadCGI: CGI actions in your monad

CGI is not the only monad in which you can use the functions from the cgi package. MonadCGI is a class of CGI monads, and there is a CGIT monad transformer. You can use these together with your own monads for things like holding database handles, keeping track of session information etc.

Database-driven web applications

HaskellDB is a combinator library for expressing database operations in Haskell. HaskellDB can be used to create database-driven web applications.

Web services

REST

XML-RPC

SOAP

FastCGI

FastCGI is a standard for CGI-like programs that are not restarted for every request. This reduces the overhead involved in handling each request, and reduces the servers response time for each request.

Setting it up

Install FastCGI.

Get a web server which can run FastCGI programs.

Import Network.FastCGI

Use runFastCGI

Persistent database connections

Since FastCGI programs are not restarted for each request, you can get persistent database connections by simply establishing the database connection outside of the call to runFastCGI.

There is one problem with this, database connection errors will not be reported as such to the user, since the exeception is thrown outside of the region protected by the error handler. This code takes care of this problem:

dbConnect :: (Database -> IO a) -> IO a
dbConnect = ...

cgiMain :: CGI CGIResult
cgiMain = ...

main :: IO ()
main = catch (dbConnect (\db -> runFastCGI (handleErrors (cgiMain db))))
             (\e -> runFastCGI (outputException e) >> throw e)