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 HTTPWe’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:
- immutable access to the current HTTP request;
- mutable access to the current HTTP response;
- an accumulated set of headers that will be returned alongside a response;
- 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 ResponseReceivedThis 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.LiftYet 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.LiftAs 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.WarpWe’ll pull in the warp web server to actually serve our requests.
import qualified Data.ByteString.Streaming as StreamingFinally, 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 IOTo 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.ok200This is a little involved, so let’s step through it slowly:
- The call to
askinvokes theReadereffect. Note that we provide it a visible type application; unlikemtl, actions expressed withfused-effectscan have multipleReaderorStateconstraints, and because of this we generally use the type application syntax to indicate to which type a call toask,gebt, orputrefers.Because we pass the yielded datum to therawQueryStringfunction, which takes aWai.Request, GHC is able to infer the type of this call toaskwithout 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 tomtlimposing only oneMonadReaderandMonadStateconstraint per action. - Similarly, we call
tellto invoke theWritereffect, providing it with a list of header-value pairs. Again, we use a visible type application to indicate both to the compiler and reader whatWriterconstraint we want to invoke. - The call to
sendMinvokes theLifteffect. Like theliftfunction provided byMonadTrans, this function lifts actions in a context’s base monad (hereByteStream) into that context. BecauseByteStreamhas anIsStringinstance, we can represent the action of sending the stringHello, world!down the pipe with the string literal"Hello, world!". We could also use theStreaming.stringhelper function if we wished to eschew theOverloadedStringsextension. - Finally, we call
putto hook into theStateeffect, setting the mutableHTTP.Statusdatum to return200 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 = doWe’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
$ actionThis 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.