All the designs seem to be driven by three main concerns:
- Requests can have streamed bodies (so we don't have to load them all in memory before starting to process them). How to best represent it?
- Responses can be streamed as well. How to best represent it?
- How to ensure that resources allocated in the production of a response are properly freed? (For example, how to ensure that file handles are freed after serving a file?)
type Application = Request -> Iteratee B.ByteString IO Response
This version uses iteratees, which were an early solution for streaming data in Haskell. Iteratee consumers had to be written in a "push-based" way, which was arguably less natural than the "pull-based" consumers used in modern streaming libraries.
The streamed body of the request is fed to the iteratee and we get a Response
value at the end. The Response
contains an enumerator (a function that feeds streamed response bytes to a response iteratee supplied by the server). Presumably, the enumerator would control resource allocation using functions like bracket
.
type Application = Request -> ResourceT IO Response
This version uses the resourcet monad transformer for resource management, instead of doing it in the enumerator. There is a special Source
type inside both Request
and Response
which handles streamed data (and which is a bit hard to understant IMHO).
type Application = Request -> IO Response
This version uses the streaming abstractions from conduit, but eschews resourcet and instead provides a bracket-like responseSourceBracket
function for handling resources in streamed responses.
type Application = Request -> (forall b. (Response -> IO b) -> IO b)
type Application = Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived
This version moves to a continuation-based approach which enables the handler function to use regular bracket
-like functions to control resource allocation. Back to square one, in that respect!
Conduits are no longer used for streaming. Now there is a plain Request -> IO ByteString
function for reading chunks of the request body, and a (Builder -> IO ()) -> IO () -> IO ()
function in the Response
for generating the response stream. (The Builder -> IO ()
write function along with a flush action are supplied by the server.)
Like the resourcet-based versions, and unlike the iteratee-based version, this implementation lets you overlap reading the request body with streaming the response.
The polymorphic handler is a neat trick to ensure that the response-taking callback Response -> IO b
is always called: the handler needs to return a b
, and the only way to get one is to actually invoke the callback!
This polymorphic solution seems to have caused some problems (perhaps with storing handlers in containers?) Instead of using polymorphism, we can use a ResponseReceived
token without a public constructor. The effect is the same: the only way for handler code to get hold of the token it needs to return is to invoke the callback.