CAL Hangman on Google AppEngine, Part 1

This post describes a simple implementation of the game ‘Hangman’, written in CAL for Google Appengine.

Introduction

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.

You can try my Hangman for AppEngine implementation to understand the game. The source is available at launchpad.

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.

Design

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

The 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 HttpServletRequest and HttpServletResponse classes.

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 HttpServletResponse:

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];

Writing Handlers

The 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.

Combinators

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 get/require Header/Cookie functions.

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;  

Example

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.

Post a Comment

Your email is never shared. Required fields are marked *

*
*