Serving HTTP Content with Fused-Effects
In the Haskell community, 2019 was the year of effect systems. From
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
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.
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
Response types; our DSL will consume
Requests and produce
http-types package provides a shared vocabulary to describe request types (
POST, etc.) and response codes (
404 Not Found, etc.)
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
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.
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
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:
These effects may be familiar to you. The
Reader effect corresponds to
State effect to
MonadTrans. (Don’t worry about
Streaming.ByteString yet.) We’ll import these effects’ definition from
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
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.
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.
We’ll pull in the
warp web server to actually serve our requests.
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
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
To disambiguate the
Lazy.ByteString type from the
Streaming.ByteString monad, we’ll define a type synonym for the
ByteString monad over
A Simple Handler
Let’s dive right in. We’ll use the effects we’ve imported to write a dead-simple web handler.
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.
This is a little involved, so let’s step through it slowly:
- The call to
Readereffect. Note that we provide it a visible type application; unlike
mtl, actions expressed with
fused-effectscan have multiple
Stateconstraints, and because of this we generally use the type application syntax to indicate to which type a call to
putrefers.Because we pass the yielded datum to the
rawQueryStringfunction, which takes a
Wai.Request, GHC is able to infer the type of this call to
askwithout 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
mtlimposing only one
MonadStateconstraint per action.
- Similarly, we call
tellto invoke the
Writereffect, 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
Writerconstraint we want to invoke.
- The call to
Lifteffect. Like the
liftfunction provided by
MonadTrans, this function lifts actions in a context’s base monad (here
ByteStream) into that context. Because
IsStringinstance, 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.stringhelper function if we wished to eschew the
- Finally, we call
putto hook into the
Stateeffect, setting the mutable
HTTP.Statusdatum to return
This isn’t a hugely interesting HTTP handler, but it’s good enough for our purposes. Our next step is to interpret this effect.
To actually serve a request, we need to call
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 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.
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.
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
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
Some More Abstractions
ConstraintKinds extension to GHC, we can give a single name to the set of effects required to express a Wai application.
This cleans up the type signatures of our handler functions considerably. We are not, however, locked into using just these effects.
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.