Serving HTTP Content with Fused-Effects

2019-11-22

In the Haskell community, 2019 was the year of effect systems. From eff to polysemy to capabilities to fused-effects, we’ve seen a whole class of libraries providing an alternative to mtl, the historical de facto choice for expressing effects in Haskell. I spoke at Strange Loop about the definition and history of effect systems, the tradeoffs associated with selecting an effect system, and why I think fused-effects is an excellent choice. The response from the Haskell community has been exciting: projects like the Aura package manager, the Komposition video editor, and the semantic analysis toolkit have adopted fused-effectsI’m personally quite pleased that this is the case, having been present at fused-effects’s inception, and having been lucky enough to contribute alongside Rob’s incredible efforts.

, and the Axel programming language is built atop polysemy.

Yet the number one complaint that Rob and I have heard from prospective users is that there doesn’t exist an end-to-end tutorial demonstrating to those already familiar with mtl the process of building applications with fused-effects. This post exists to fill that particular need: I’ll show you how to use fused-effects’s APIs to build a minimal but aesthetically-pleasing syntax for handling HTTP requests and serving HTTP content.

This post will take an ad-hoc approach to the problem. While fused-effects often shines brightest when used in conjunction with user-defined, situationally-appropriate effect types, it’s profoundly useful for small, one-off tasks. The syntactic tools provided by fused-effects itself are often wholly adequate for taming the complexity associated with a complicated function signature or unwieldy API. For simplicity’s sake, we won’t bother with request routing, caching, or any of the features associated with apps built on actual web frameworks.

Let’s get started. This post is literate Haskell; you can find its source here. We’ll call this little web app Quad, in homage to another quick-and-dirty software product of yesteryear.

{-# LANGUAGE TypeApplications, OverloadedStrings #-}
module Web.Quad where

import qualified Network.Wai as Wai
import qualified Network.HTTP.Types as HTTP

We’ll use the wai framework to abstract over the interface over whatever web server we end up using; like its Python cousin WSGI or Ruby’s rack, Wai is little more than a shared set of types and calling conventions for communicating with a web server. It defines Request and Response types; our DSL will consume Requests and produce Responses. The http-types package provides a shared vocabulary to describe request types (GET, POST, etc.) and response codes (200 OK, 404 Not Found, etc.)

Desiderata

The list of things that an app atop a web server can do is considerable. We don’t have the space, time, or inclination to write a full-fledged Rails clone here, so let’s err on the side of the minimal. The sparsest possible vocabulary associated with an HTTP request handler involves these items:

  1. immutable access to the current HTTP request;
  2. mutable access to the current HTTP response;
  3. an accumulated set of headers that will be returned alongside a response;
  4. and a way to respond to the client with a stream of bytes.

The process of building programs with fused-effects involves mapping desired program behavior to a set of one or more effects, then interpreting those effects into a result data type. Depending on your goals, you may be able to use the effects and monads that come with fused-effects, or you may have to write your own effects. As mentioned above, this post will use an ad-hoc approach, building this app’s capabilities atop the effects provided by fused-effects’s core.

Having identified these four requirements, we now need to know how to represent them in code. To do this, we need to consult wai’s most fundamental definition.

-- from Network.Wai
type Application
  =  Request
  -> (Response -> IO ResponseReceived)
  -> IO ResponseReceived

This declaration defines an Application type. Values of type Application are functions that take two arguments. This first argument is an immutable Request; the second is a function that takes a Response and returns an opaque ResponseRecieved type.

We access the properties of the current request through accessors provided by Network.Wai, and we yield a ResponseReceived by constructing a Response datum and passing it to the provided function. This means that whatever effectful abstraction we choose, the result of interpreting an effectful action will be an Application—a function type that we pass into our chosen HTTP server. Our task now is to identify which effects we will use to pair a type provided by wai with its corresponding capability:

Capability Wai type Effect
Immutable requests Wai.Request Reader Wai.Request
Mutable responses HTTP.Status State HTTP.Status
Accumulated headers Wai.ResponseHeaders Writer Wai.ResponseHeaders
Streaming responses Streaming.ByteString Lift Streaming.ByteString

These effects may be familiar to you. The Reader effect corresponds to mtl’s MonadReader, the State effect to MonadState, Writer to MonadWriter, and Lift to MonadTrans. (Don’t worry about Streaming.ByteString yet.) We’ll import these effects’ definition from fused-effects.

import Control.Effect.Reader
import Control.Effect.State
import Control.Effect.Writer
import Control.Effect.Lift

Yet it’s not enough just to import these effects. One of the primary wins associated with fused-effects is that it separates the interfaces associated with an effect from the implementation of that effect. One effect can have multiple interpretations; depending on our needs, we could interpret a state effect with a strict state monad, or a lazy state monad, or a reader monad wrapping a mutable reference. We call these monads that interpret an effect a carrier. These carriers live under fused-effects’s Control.Carrier hierarchy. Let’s import the carriers we need: we’ll be using strict state and writer monads, since we don’t need the generality provided by lazy state, and the lazy writer monad shouldn’t exist in the first place.

import Control.Carrier.Reader
import Control.Carrier.Strict.State
import Control.Carrier.Writer
import Control.Carrier.Lift

As it happens, the above carrier modules reexport their corresponding effects, so the above imports from Control.Effect are not necessary: you only need O(n) imports, not O(2n). You’re welcome.

import qualified Network.Wai.Handler.Warp

We’ll pull in the warp web server to actually serve our requests.

import qualified Data.ByteString.Streaming as Streaming

Finally, we’ll pull in the streaming-bytestring library to provide a nice interface for streaming data over the wire. Haskell has many choices for streaming data; we could have used io-streams or pipes-bytestring, but streaming-bytestring is convenient in that its underlying type, the ByteString monad transformer, represents computations that involve streamed bytes. By using the ByteString monad as the base effect in our effect stack, we can use the Lift effect to abstract over the action of streaming bytes into the body of a Response.

type ByteStream = Streaming.ByteString IO

To disambiguate the Lazy.ByteString type from the Streaming.ByteString monad, we’ll define a type synonym for the ByteString monad over IO.

A Simple Handler

Let’s dive right in. We’ll use the effects we’ve imported to write a dead-simple web handler.

helloWorld :: ( Has (Reader Wai.Request) sig m
              , Has (State HTTP.Status) sig m
              , Has (Writer Wai.ResponseHeaders) sig m
              , Has (Lift ByteStream) sig m
              )
           => m ()

Declaring this signature establishes helloWorld as a monadic action m returning no interesting result (the unit type ()). We declare the capabilities of this handler piecewise by using the Has constraint: a Has eff sig m constraint declares that the monad m has access to the effect eff in the given signature sig. (We’ll touch more on what signatures mean in fused-effects later; you can ignore them for now). (Note that we don’t return a result here, even though wai expects us ultimately to return a ResponseReceived datum, because we’ll build that datum when we interpret the helloWorld action into a concrete type.) Now that we have a signature for this action, we can define a minimally-interesting body for it.

helloWorld = do
  req <- ask @Request
  tell @Wai.ResponseHeaders [(HTTP.hContentType, "text/plain")]
  sendM "Hello, world!\n"
  sendM ("You requested " <> Streaming.fromStrict (Wai.rawQueryString reqd))
  put @HTTP.Status HTTP.ok200

This is a little involved, so let’s step through it slowly:

  • The call to ask invokes the Reader effect. Note that we provide it a visible type application; unlike mtl, actions expressed with fused-effects can have multiple Reader or State constraints, and because of this we generally use the type application syntax to indicate to which type a call to ask, gebt, or put refers.Because we pass the yielded datum to the rawQueryString function, which takes a Wai.Request, GHC is able to infer the type of this call to ask without the explicit type application; I’ve kept it in there both for pedagogy’s sake and out of personal preference.

    This is very handy in that it lets us manipulate exactly what state and context types we need, without having to resort to the “classy-lenses” approach due to mtl imposing only one MonadReader and MonadState constraint per action.
  • Similarly, we call tell to invoke the Writer effect, providing it with a list of header-value pairs. Again, we use a visible type application to indicate both to the compiler and reader what Writer constraint we want to invoke.
  • The call to sendM invokes the Lift effect. Like the lift function provided by MonadTrans, this function lifts actions in a context’s base monad (here ByteStream) into that context. Because ByteStream has an IsString instance, we can represent the action of sending the string Hello, world! down the pipe with the string literal "Hello, world!". We could also use the Streaming.string helper function if we wished to eschew the OverloadedStrings extension.
  • Finally, we call put to hook into the State effect, setting the mutable HTTP.Status datum to return 200 OK.

This isn’t a hugely interesting HTTP handler, but it’s good enough for our purposes. Our next step is to interpret this effect.

Interpretation

main :: IO ()
main = Warp.run 8080 (runApplication helloWorld)

To actually serve a request, we need to call warp’s run function, which takes a port number and a Wai.Application to run. This is not rocket science, but it does pose us a problem: we need to define a runApplication function if we want to actually compile this. At this point, fused-effects’s idioms start diverging from those of mtl.

-- mtl style
newtype WebT m a = WebT { unWebT :: WriterT Wai.ResponseHeaders (StateT HTTP.Status (Streaming.ByteString m)) a }
  deriving (Functor, Applicative, Monad, MonadState HTTP.Status, MonadWriter Wai.ResponseHeaders, MonadTrans)

In an mtl universe, we’d define our own monad transformer, and we’d use the GeneralizedNewtypeDeriving extension to conform to the various MonadFoo interfaces. We can build our fused-effects applications with this kind of concrete monad stack, and sometimes we may wish to do so, but for this case the particulars of our monad stack aren’t particularly interesting. In this case, we want to abstract over the particulars of what concrete monad stack we use. As such, we’ll use the PartialTypeSignatures extension to leave this type purposely abstract: by prefixing our monad type m with an underscore, GHC will infer from our interpretation functions what concrete type to use.

runApplication :: _m () -> Application
runApplication action req respond = do

We’re going to use the functions provided by the imported Control.Carrier modules to interpret action into the types we need to build a ResponseReceived datum. These functions obey the naming convention established by the transformers package, though their parameter orders have been changed to make composition easier.

result <-
  ByteStream.toLazy
  . runM @ByteStream
  . runReader @Wai.Request req
  . runState @HTTP.Status HTTP.status500
  . runWriter @Wai.Response
  $ action

This will be immediately familiar to people who, like me, have spent dozens and dozens of hours wrapping and unwrapping mtl transformer stacks. But there are some immediate differences. Note, for example, that we’ve specified the order of effects not with a data structure, as in the WebT monad above, but with the calls to the run family of functions. Because the . operator works right-to-left, we start by discharging the Writer effect from action: this uses the Control.Carrier.Writer.Strict carrier to peel one layer of effects off of action. That carrier preserves all the result of all aggregated Writer actions (such as tell or listen) into a Wai.ResponseHeaders datum returned in a tuple. Later, when we need to construct a Response, we’ll deconstruct result and extract that datum.

After peeling off that Writer effect, we then peel off a State effect. We pass a type application for explicitness’s sake, along with an initial datum with which this state value will be initialized (in this case status500). We then peel off the Reader effect, passing the provided request data to runReader. Finally, we discharge the Lift effect with runM, yielding a ByteStream value, which we then interpret into a lazy bytestring paired with the status and header information.

At this point, result is of type Of Lazy.ByteString (ResponseHeaders, (Status, ())). The Of type comes from streaming-bytestring, where it represents a left-strict pair; the nested tuples represent the data yielded at each effect discharge, terminating in the unit value. With a case statement and some helper functions, we can build a Response and pass it to respond.

let (respBody :< (headers, (status, ()))) = result
respond (Wai.responseLBS status headers respBody)

Some More Abstractions

type Web sig m = ( Has (Reader Wai.Request) sig m
                 , Has (State HTTP.Status) sig m
                 , Has (Writer HTTP.ResponseHeaders) sig m
                 , Has (Lift ByteStream) sig m
                 )

With the ConstraintKinds extension to GHC, we can give a single name to the set of effects required to express a Wai application.

htmlHandler :: Web sig m => m ()
htmlHandler = do
  put @HTTP.Status HTTP.status200
  sendM "<html><h1>Hello.</h1></html>“

This cleans up the type signatures of our handler functions considerably. We are not, however, locked into using just these effects.

-- Assume we have some 'Config' data type providing a 'responseFromConfig' function.
htmlHandler :: (Has (Reader Config) sig m, Web sig m) => m ()
htmlHandler = do
  cfg <- ask @Config
  put @HTTP.Status HTTP.status200
  sendM (responseFromConfig cfg)

We’re able to add a new Reader constraint to a handler, even though we already have a Reader constraint in the Web synonym, because fused-effects is just that versatile. (This would not be possible to do with the WebT monad transformer.)

At this point, we have enough code to run these actions. Let’s do so:

$ curl localhost:8080/?query
Hello, world!
You requested: ?query

Is this an exciting web application? No. It provides very few features and no request routing at all. Furthermore, it’s an admittedly ad-hoc design. A better and more morally-upstanding design would define custom effects for the four capabilities of our web server. And indeed we will do that… next time.