Hangman is a simple word guessing game — the computer chooses a word, telling you only how many letters it has. You that it contains a particular letter. Correct guesses fill in those letters in the word, until you have guessed all the letters in the word, or you make too many incorrect guesses and lose.
Hangman is a very simple game — the state is just the word being guessed, and the letters guessed so far. The point of implementing it in CAL on AppEngine is to develop CAL modules for the AppEngine API classes.
It would be easy to implement hangman as a mostly client-side browser based application, but that wouldn’t have exercised much of the AppEngine API.
To make the task interesting, I decided that the game state would be persisted on the server, and a user’s current game would be identified by a cookie in their browser. So you can make some guesses, close your browser, and come back to the same game later.
Http module provides function for creating a server which handles requests and creates responses. The source is here.
The Server Function
All requests go through a single servlet, which is configured with
init-params to reflectively call a static method generated by the CAL stand-alone jar builder.
The function called by the servlet must have the type
HttpServletRequest -> JHttpServletResponse. These CAL types are simply imports of the Java
data foreign unsafe import jvm "javax.servlet.http.HttpServletRequest" public HttpServletRequest deriving Inputable, Outputable; data foreign unsafe import jvm "javax.servlet.http.HttpServletResponse" private JHttpServletResponse deriving Inputable, Outputable;
JHttpServletResponse is private to the module because it is mutable and so shouldn’t be exposed to clients of the module. Instead
Http provides a data type
data public HttpServletResponse = private HttpServletResponse cookies :: [Cookie] headers :: [Header] body :: HttpServletResponseBody; data public HttpServletResponseBody = private Ok contentType :: String content :: (Array Byte) | private Error code :: Int message :: (Maybe String) | private Redirect url :: String;
This allows the client to assemble the state of a response, which is then applied to the response in a single operation by the
Http module. A monad to sequence operations on the Java response object would have been another alternative, but that wouldn’t enforce correct construction of the response to the same degree — e.g. the client might not set a content type. Note that the actual constructors are private so that the structure of the type is not exposed to clients of this module.
The current data type doesn’t allow the response to be streamed.
As clients can’t see or use
JHttpServletResponse they need to use the
server function to create a web server:
server :: [HttpServletRequest -> Maybe HttpServletResponse] -> HttpServletRequest -> JHttpServletResponse -> ();
This function takes a list of handlers, each of which is tried in sequence until one returns a response. So the server function for the hangman web application is:
public service = server [currentGameHandler, newGameHandler, guessHandler, restartHandler];
Http module provides functions to test and retrieve properties of the request, and combinators to build handlers from these individual functions. The combinators are inspired by Mark Tullsen’s First Class Patterns.
Each combinator combines two functions, each of which extracts a property of the request, and returns a function of the type
HttpServletRequest -> Maybe a.
It isn’t clear to me that this is the very best approach — experimenting with a
Maybe/Reader monad stack would be interesting.
hOr :: (HttpServletRequest -> Maybe a) -> (HttpServletRequest -> Maybe a) -> (HttpServletRequest -> Maybe a);
hOr requires that at least one of two extraction functions succeeds, i.e. returns
Just x. The final result is the result of the first extraction function to succeed (if the first extraction function succeeds, the second is not evaluated).
hThen :: (HttpServletRequest -> Maybe a) -> (HttpServletRequest -> Maybe b) -> (HttpServletRequest -> Maybe b);
hThen requires that both the extraction functions succeed, but it discards the result of the first extraction function.
hAnd :: (HttpServletRequest -> Maybe a) -> (HttpServletRequest -> Maybe b) -> (HttpServletRequest -> Maybe (a,b));
hAnd requires that both extraction functions succeed, and returns their results as a pair.
hNot :: (HttpServletRequest -> Maybe a) -> (HttpServletRequest -> Maybe ());
hNot reverses the sense of an extractor, succeeding if it fails and vice-versa.
The following combinators have specialised functions, rather than acting to combine two extraction functions.
hCompose :: (HttpServletRequest -> Maybe a) -> (a -> b) -> (HttpServletRequest -> Maybe b);
hCompose transforms the result of an extraction function.
hApply :: (HttpServletRequest -> Maybe a) -> (a -> HttpServletResponse) -> (HttpServletRequest -> Maybe HttpServletResponse);
hApply combines a sequence of extraction functions with a function that produces a response.
hAttempt :: (HttpServletRequest -> Maybe a) -> (a -> Maybe HttpServletResponse) -> (HttpServletRequest -> Maybe HttpServletResponse);
hAttempt is like
hApply, but the response generation function returns
Maybe HttpServletResponse, allowing further conditional processing outside the context of combined extraction functions.
Request Attribute Extractors
These functions look at attributes of the response and return their values (which may amount to just signalling their presence or absence).
getParameter :: String -> HttpServletRequest -> Maybe (Maybe String);
getParameter extracts the value of a named parameter from the request. It always succeeds, returning
Just Nothing or
Just $ Just paramValue.
requireParameter :: String -> HttpServletRequest -> Maybe String;
requireParameter extracts the value of a named parameter, but fails (i.e. returns
Nothing) if it is not set.
There are similar
matchUrl :: String -> HttpServletRequest -> Maybe ();
matchUrl takes a string containing a regular expression, and succeeds if the regular expression matches the path info of the request. It would be useful to have another function which returns the values of capturing groups of the regular expression.
Creating the Response
Utility functions are provided for each common response type.
ok :: HasByteRep a => String -> a -> HttpServletResponse; public ok contentType content = ... textHtml :: HasByteRep a => a -> HttpServletResponse; public textHtml = ok "text/html"; redirect :: String -> HttpServletResponse; public redirect url = ... err :: Int -> Maybe String -> HttpServletResponse; public err code msg = ...
These functions all create requests with default headers. Headers and Cookies are added by applying a function to the request.
addHeader :: Header -> HttpServletResponse -> HttpServletResponse; addCookie :: Cookie -> HttpServletResponse -> HttpServletResponse;
Rather than an example from the hangman game, here’s a simple example which uses the functions discussed directly.
helloHandler = let page p = let (firstName, maybeLastName) = p; in ok "text/plain" ( case maybeLastName of Just lastName -> "Greetings, " ++ firstName ++ " " ++ lastName; Nothing -> "Hi there " ++ firstName; ); in matchUrl "/hello" `hThen` (requireParameter "firstName") `hAnd` (getParameter "lastName") `hApply` page;
This handler will return a page for the url
/hangman/hello if the
firstName parameter is present. To keep the example as simple as possible it uses the
text/plain content type to avoid the verbiage of an HTML page.
To be continued…
In the next instalment I’ll describe the CAL module which supports the AppEngine datastore.