7 Web Programming

7.6 Stateful Forms

The simple HTML forms presented so far are quite limited. Form definitions are top-level entities so that their layout is fixed, i.e., they cannot show data from a possible usage context, e.g., database entities or authentication data. In order to make forms more flexible, it should be possible that forms access some state to be shown in the form. Such stateful forms need only to read data, since the manipulation of data, e.g., updating a data base depending on the user input, is usually performed by the event handlers (therefore, event handlers are of type IO).

To support stateful forms, the library HTML.Base defines a monad FormReader with operations to read data (see below), i.e., the FormReader monad is a restriction of the IO monad where only read operations are supported.33 3 The restriction to read operations is necessary since forms might be executed several times, e.g., to construct the form layout, to start an event handler, or if the form has multiple occurrences in a web page. Thus, a form should not change the environment. Changes are only performed by the activation of some event handler. Thus, a stateful form consists of a FormReader action to read data of some type a and an operation which maps values of this type into an HTML form. The library HTML.Base supports the definition of stateful forms by the form constructor operation

formDef :: FormReader a -> (a -> [HtmlExp]) -> HtmlFormDef a

Therefore, an operation of type HtmlFormDef a specifies a form which reads some data of type a and use this data to generate the actual HTML form.

In order to define a stateful form, we need an operation of type FormReader a. A difficulty in web programming is the fact that HTTP is a stateless protocol, i.e., each web page can be shown several times and there are no connections between different web pages to pass some state from one page to another. This problem is solved in typical web applications by a session concept where each user has its own session where session-local data can be stored, like the login name of a user or the contents of a virtual shopping basket. A session concept can be implemented via cookies to identify the client in a session. To hide the details of session handling, the package html2 contains the library HTML.Session which provides the following operations (the type variable a denotes the type of session data):

getSessionData    :: (Read a, Show a) => SessionStore a -> a -> FormReader a
putSessionData    :: (Read a, Show a) => SessionStore a -> a -> IO ()
removeSessionData :: (Read a, Show a) => SessionStore a -> IO ()

The type variable a specifies the type of session data. The class constraints Read and Show are required since the data is stored on the web server. “SessionStore a” is the type of a top-level entity referring to some memory cell containing session information of type a. getSessionData retrieves information of the current session (and returns the second argument if there is no information, e.g., in case of a new session), putSessionData stores information in the current session, and removeSessionData removes such information. A session store containing data of type a can be defined in a Curry program as a top-level entity by using the constructor operation

sessionStore :: (Read a, Show a) => String -> SessionStore a

The argument is a unique name (consisting of letters and digits) for the session store in the web application.44 4 Session stores are implemented as persistent global entities, see Curry package global, which are stored in the file system of the server.

In order to see an application of this concept, we implement a simple number guessing game: the client has to guess a number known by the server (here: 42), and for each number entered by the client the server responds whether this number is correct, smaller or larger than the number to be guessed. If the guess is not right, the answer form contains an input field where the client can enter the next guess. Moreover, the number of guesses should also be counted and shown at the end. Hence, the session state contains the number of trials and is defined as

trials :: SessionStore Int
trials = sessionStore "trials"

The form definition consists of an action that reads the current session data and the HTML form for this data [Browse Program][Download Program]:

guessForm :: HtmlFormDef Int
guessForm = formDef (getSessionData trials 1) guessFormHtml
guessFormHtml :: Int -> [HtmlExp]
guessFormHtml t =
  [htxt "Guess a number: ", textField nref "",
   button "Check" guessHandler]
 where
  nref free
  guessHandler env = do
    let n = read (env nref)
    if n==42
      then do
        removeSessionData trials
        return $ headerPage ("Correct! " ++ show t ++ " guesses!") []
      else do
        putSessionData trials (t+1)
        return $ headerPage ("Too " ++ if n<42 then "small!" else "large!")
               [formElem guessForm ]

The form handler reads the user input from the HTML environment (one could add an additional check whether the input is a number string) and compares it with the “secret” number. If it is equal, the session data is removed before returning the answer, otherwise the session data is updated so that the next form invocation gets the updated data.