This article pre-supposes that you have some basic knowledge of Haskell, Rack/PEP 333, and middleware.


Middleware, as a plugin architecture for creating custom web stacks, is an amazing tool (the fact that it makes web servers interchangeable is equally important but not useful for this discussion). By simplifying and standardizing the interface used by middleware to communicate, web developers have been given an easy way to compose and reuse nicely sized chunks of functionality. Interestingly the pattern itself is general enough that it applies just as well to pluggable architectures outside of the web stack as evidenced by recent additions of similar functionality to Vagrant. In fact if you stand back a bit further, just far enough to look past object oriented underpinnings, it starts to look a lot like function composition which many consider to be the pinacle in abstractions for software reuse.


To illustrate the similarities with function composition lets define a simple Rack app with some middleware borrowed from an introductory tutorial on Rack middleware.
# A sample rack app
app = do
  use Rack::Upcase
  use Rack::Reverse

  run lambda { |env| [200, { 'Content-Type' => 'text/html' }, 'Hello World'] }
The middleware used here modify the response in the manner you would expect. As long as they both forward the request environment down the stack they are afforded the opportunity to manipulate the response provided, first, by the endpoint and then by any subsequent middleware. Now something similar, though simpler, in Haskell:
import Data.Char
import Prelude hiding (reverse)
import qualified Data.List as List

type Response = (Int, [(String, String)], String)

run = reverse . upcase . \env -> (200, [("Content-Type", "text/html")], "Hello World!")

reverse :: Response -> Response
reverse (s, h, body) = (s, h, (List.reverse body))

upcase :: Response -> Response
upcase (s, h, body) = (s, h, ( toUpper body))

-- *Main> run ()
-- (200,[("Content-Type","text/html")],"!DLROW OLLEH")
-- *Main>
There obviously isn't any environment forwarding performed here, but the response manipulation is virtually the same. Both the Rack middleware and the composed functions form a pipeline of functionality bound by the common expectation of an HTTP response type in the form of a triple. As long as whatever you're building is able to manipulate that triple it can be used at any stage in the pipeline or in any other pipeline with the same expectation. But this basic composition, a wonderful tool though it is for creating reusable and generic software, still leaves some issues unresolved.


When building your own Rack middleware there are a couple of ways you can go about handling errors or problematic states. First, you can simply return an error response, thereby alerting the outside world to problems immediately. Alternatively you can attach error information to the environment and allow subsequent middlwares to decide what they would like to do and then possibly modify the response when one is returned. Both options leave something to be desired because there are no guarantees or contracts around error handling. To illustrate lets address the first scenario, see how it maps to function composition and then attempt to improve it. To do this we'll add another bit of middleware into our stack, one that has a condition under which it cannot operate and must report a failure. We'll call it Rack::Head, and it will attempt to trim the response body down to its first character.
module Rack
  class Head
    def initialize app
      @app = app

    def call env
      status, headers, body = env

      if body.empty?
        [500, headers, "Head cannot operate on an empty string"]
        [status, headers, [body.first]]
Now, if the endpoint's response body is changed to be an empty string this new middleware will have to report its failure to those above it in the stack.
app = do
  use Rack::Upcase
  use Rack::Reverse
  use Rack::Head

  run lambda { |env| [200, { 'Content-Type' => 'text/html' }, ''] }
Unfortunately there is no guarantee the middleware above Rack::Head won't just replace, alter, or in this case reverse the response body making the error response hard to read or non-existent. This, I believe, is an intentionaly flexible design choice leaving what to do with errors in the hands of middleware authors. Looking back to Haskell we get a similarly unfortunate result with pure function composition:
run = reverse . upcase . head . \env -> (200, [("Content-Type", "text/html")], "")

head :: Response -> Response
head (s, h, "") = (s, h, "Head cannot operate on an empty string")
head (s, h, body) = (s, h, [List.head body])

-- *Main> run ()
-- (200,[("Content-Type","text/html")],"GNIRTS YTPME NA NO ETAREPO TONNAC DAEH")
-- *Main>
Here the application accounts for the fact that head is unable to operate on an empty list, an error message replaces the response body, and is subsequently mangled. The application could let head throw its empty list exception, though thats less than optimal as well. It appears that simple composition has reached the limits of its usefulness in error handling and some other tactic is required.


While Rack, in its current form, doesn't have any alternatives to offer when it comes to error handling, Haskell has some interesting tools that might provide some insight. The first, and most oft cited, is the Maybe monad. When used in conjunction with the monad instance of Maybe, functions can continue to use composition, if in a slightly altered form, without much additional overhead for the developer while the pipeline is imbued with the ability to "short circuit" when something untoward occurs1.
import Control.Monad

run = reverse <=< upcase <=< head <=< \env -> Just (200, [("Content-Type", "text/html")], "")

reverse :: Response -> Maybe Response
reverse (s, h, body) = Just (s, h, (List.reverse body))

upcase :: Response -> Maybe Response
upcase (s, h, body) = Just (s, h, ( toUpper body))

head :: Response -> Maybe Response
head (s, h, "") = Nothing -- (s, h, "Head cannot operate on an empty string")
head (s, h, body) = Just (s, h, [List.head body])

-- *Main> run ()
-- Nothing
-- *Main> 
There are 3 changes to the original. First, an import of Control.Monad makes the Maybe Monad instance available. Second, the dot composition operator has been replaced with the monad composition operator (<=<). Third, the type of (<=<) is
(<=<) :: (Monad m) => (b -> m c) -> (a -> m b) -> a -> m c
so we are required to wrap our Response with one of the two Maybe value constructors (Maybe is the m in the type signature). If this seems convoluted, just remember that the type of the regular composition operator (.) is
(.) :: (b -> c) -> (a -> b) -> a -> c
and the only real difference is that we're required to wrap the result in the Maybe monad. The last is the key to how the head function can prevent the other middleware from altering its error response. The monadic composition operator (<=<) is built on the monadic bind operator, (>>=) which is defined in the Monad instance for Maybe:
instance Monad Maybe where
    return         = Just
    fail           = Nothing
    Nothing  >>= f = Nothing
    (Just x) >>= f = f x
The key is that when Nothing is passed as the first argument to (>>=) the result will always be Nothing. Consequently we can say the same about (<=<) as the only extra work that it does on top of the bind operator is some type unwrapping. So! by making sure to wrap the return result in the Maybe monad with the Just data constructor, or Nothing data constructor if there is a failure, the plumbing for managing errors (ie "short circuiting" ) is handled in the (<=<) operator.
run = reverse <=< upcase <=< head <=< \env -> Just (200, [("Content-Type", "text/html")], "")

-- *Main> run ()
-- Nothing
-- *Main>

run2 = reverse <=< upcase <=< head <=< \env -> Just (200, [("Content-Type", "text/html")], "Hello World!")

-- *Main> run2 ()
-- Just (200,[("Content-Type","text/html")],"H")
-- *Main>
Great! We've established a way to prevent the other middleware from futzing with the error, but lets make one small change so that our pipeline can report something more useful than Nothing while continuing to short circuit.
import Control.Monad.Error

reverse :: Response -> Either String Response
reverse (s, h, body) = Right (s, h, (List.reverse body))

upcase :: Response -> Either String Response
upcase (s, h, body) = Right (s, h, ( toUpper body))

head :: Response -> Either String Response
head (s, h, "") =  Left "Head cannot operate on an empty string"
head (s, h, body) = Right (s, h, [List.head body])

run = reverse <=< upcase <=< head <=< \env -> Right (200, [("Content-Type", "text/html")], "")

-- *Main> run ()
-- Left "Head cannot operate on an empty string"
-- *Main>

run2 = reverse <=< upcase <=< head <=< \env -> Right (200, [("Content-Type", "text/html")], "Hello World!")

-- *Main> run2 ()
-- Right (200,[("Content-Type","text/html")],"H")
-- *Main>
The only thing thats changed here is that Either has replaced Maybe as our Monad of choice. It should come as no surprise that its defined in Control.Monad.Error, and it has two data constructors just like Maybe; Left for signaling failure and Right for signaling success. As a result we can include a message for the operator of the function to give them some helpful debugging information without worrying about another function farther up the line meddling!


Next time I'd like to take the concepts we've applied here to our Haskell "middleware" and show how abstracting the error handling in a Builder class like Rack's can provide benefits for use in something like Vagrant. Most of all I hope that this might provide some clarity around why function composition, and by extension middleware, is a really nice patterns for creating modular software.


1. Functions composed in the Maybe monad, as in the example, don't actually short circuit. They have to run through each composition but as you can see from the Maybe Monad instance they don't execute the function itself. 2. Replacing our Response whole sale with a Left String is really only useful for illustration in this case. It would be better to at least use Either Response Response, and provide an errorResponse function that takes a string and sets the status and headers properly.


22 Jul 2010