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-effects
I’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 Request
s and produce Response
s. 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 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.
= do
helloWorld <- ask @Request
req @Wai.ResponseHeaders [(HTTP.hContentType, "text/plain")]
tell "Hello, world!\n"
sendM "You requested " <> Streaming.fromStrict (Wai.rawQueryString reqd))
sendM (@HTTP.Status HTTP.ok200 put
This is a little involved, so let’s step through it slowly:
- The call to
ask
invokes theReader
effect. Note that we provide it a visible type application; unlikemtl
, actions expressed withfused-effects
can have multipleReader
orState
constraints, and because of this we generally use the type application syntax to indicate to which type a call toask
,gebt
, orput
refers.Because we pass the yielded datum to therawQueryString
function, which takes aWai.Request
, GHC is able to infer the type of this call toask
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 tomtl
imposing only oneMonadReader
andMonadState
constraint per action. - Similarly, we call
tell
to invoke theWriter
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 whatWriter
constraint we want to invoke. - The call to
sendM
invokes theLift
effect. Like thelift
function provided byMonadTrans
, this function lifts actions in a context’s base monad (hereByteStream
) into that context. BecauseByteStream
has anIsString
instance, we can represent the action of sending the stringHello, world!
down the pipe with the string literal"Hello, world!"
. We could also use theStreaming.string
helper function if we wished to eschew theOverloadedStrings
extension. - Finally, we call
put
to hook into theState
effect, setting the mutableHTTP.Status
datum 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 ()
= Warp.run 8080 (runApplication helloWorld) main
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
= do runApplication action req respond
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 ()
= do
htmlHandler @HTTP.Status HTTP.status200
put "<html><h1>Hello.</h1></html>“ sendM
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 ()
= do
htmlHandler <- ask @Config
cfg @HTTP.Status HTTP.status200
put 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.