Spock's monads and type parameters

A Spock program has two phases: registering routes when the application is built, and running an action for each matching request. Both phases can use the application’s shared services. The monad in a type signature tells you which of these jobs a function does.

Type When it runs What it provides
SpockM conn sess st a Application setup Register routes, hooks, and middleware
SpockAction conn sess st a A request Read the request, use the visitor’s session, send a response
WebStateM conn sess st a When a caller runs it Shared application state and database services

The parameters describe your application: conn is a database connection (() with PCNoDatabase), sess is one visitor’s session value, and st is shared application state. The final a is the computation’s result; a route registration block or completed handler normally uses ().

WebStateM reads an existing application environment. It does not make an immutable st mutable. Put an IORef or TVar inside st, with appropriate synchronization, when requests need to update shared values.

Sharing a helper between setup and requests

This helper reads the application’s greeting prefix:

prefix :: WebStateM () () Text Text
prefix = getState

Both registration and actions sit over WebStateM. lift prefix enters that base monad. Its location determines when it runs:

app :: SpockM () () Text ()
app = do
  configured <- lift prefix -- Once, when building the application.
  get "setup" $ text configured
  get "hello" $ do
    current <- lift prefix -- For each request to /hello.
    text current

Import lift from Control.Monad.Trans.Class. Use liftIO, from Control.Monad.IO.Class, for ordinary IO such as reading an IORef; it works through all the intervening layers. lift moves through one layer. getState and runQuery themselves are polymorphic in HasSpock, so you can also call them directly in either registration or an action. A helper with a concrete WebStateM signature needs lift there.

WebStateM has no current request, response, hook context, or visitor session. Keep code using header, jsonBody, readSession, or text in an action. Sending a response ends the action: code after text or json does not run.

Typed context from a prehook

SpockCtxM ctx conn sess st a and SpockActionCtx ctx conn sess st a add a request-local ctx. Their shorter names above set ctx to (). A prehook computes context for a matching request, and getContext reads it inside the hooked routes:

newtype VisitorName = VisitorName Text

visitor :: SpockAction () () Text VisitorName
visitor = VisitorName . fromMaybe "visitor" <$> header "X-Display-Name"

namedRoutes :: SpockCtxM VisitorName () () Text ()
namedRoutes = get "hello" $ do
  VisitorName name <- getContext
  greetingPrefix <- lift prefix
  text (greetingPrefix <> ", " <> name <> "!")

app :: SpockM () () Text ()
app = prehook visitor namedRoutes

The display-name header here is user input, not authentication. A real authentication hook would validate credentials before producing its context. Context is scoped to the hooked routes and the individual request. It is different from both application-wide st and a visitor’s persistent sess. lift does not change the context type; use prehook to supply a new context.

Core Spock and custom base monads

Full Spock specializes core Spock to WebStateM conn sess st:

type SpockCtxM ctx conn sess st = SpockCtxT ctx (WebStateM conn sess st)
type SpockActionCtx ctx conn sess st = ActionCtxT ctx (WebStateM conn sess st)

In core Spock, SpockT m a registers routes and ActionT m a handles requests over a base monad you choose. SpockCtxT ctx m a and ActionCtxT ctx m a are their context-aware forms. For example, SpockT (ReaderT Text IO) () can use lift ask inside an action; pass a runner such as flip runReaderT "hello" to spockT. The runner must work for every result type.

WebStateT conn sess st m a is the transformer used to build WebStateM, whose underlying monad is ResourceT IO. Applications usually need neither to construct this stack nor to unwrap it. For an existing full Spock environment, getSpockHeart and runSpockIO let an IO caller run a shared service computation; this still does not create a request context.

Run the examples

The repository contains a complete, tested example and tests of both full and core Spock. From the repository root:

cabal run spock-monads-example
curl -H 'X-Display-Name: Alex' http://localhost:8080/hello
cabal test Spock

See the API reference for the complete types and available request, session, and response helpers.