RESTful Content

June 18, 2010

GravatarBy Michael Snoyman

I've said many times that Yesod is based on RESTful principles. One example is the 1 resource == 1 URL design. Another is multiple representations. In my last post I described the Handler monad; here I hope to explain why the return type of handler functions usually looks like Handler MyApp RepHtml.

Files and Enumerators

Yesod is built on top of WAI, so we need to look down at that level a bit to get an understanding of what's going on. WAI is designed for performance, and in particular offers two ways of giving a response body:

  • A file path. This allows the web server to use a sendfile system call if it so desires. It can be a massive performance win since the data doesn't need to be copied at all.
  • An enumerator. Enumerators are simulatenously the cool new kid on the block, not well understood, and completely non-standard. I'm guessing there's easily a dozen enumerator definitions floating around. WAI uses one of the simplest definitions around. However, I won't really be discussing that design in this post.

Yesod therefore also allows both files and enumerators for the output; this is the Content data type. Yesod also has a ToContent typeclass (as of 0.3.0; it used to just be a more general ConvertSuccess) for converting the "usual suspects" like lazy bytestrings or text into Content.


A representation of data then really consists of two pieces of information: the Content and the mime-type. In Yesod 0.3.0, we use a simple String to represent mime-type: type ContentType = String. So how do we allow multiple representations? Let's start off with the simplest approach: [(ContentType, Content)]. Seems perfect: if a handler could return either HTML or JSON content, it would return something like:

    [ ("text/html", toContent "<p>Hi there!</p>")
    , ("application/json", toContent "{\"msg\":\"Hi there!\"}")

So how would Yesod know which representation to serve? RESTfully of course! We parse the Accept HTTP request header, determine the prioritized list of expected mime-types, and then select the appropriate representation based on that list. If none of our representations match that list, we just serve the first one.

ChooseRep and RepHtml

This is all well and good, and earlier versions of Yesod worked this way. However, you end up losing type information: I can't look at the return type of a handler and know what type of content it has. So instead, let's look at this approach:

type ChooseRep = [ContentType] -> IO (ContentType, Content)
class HasReps a where
   chooseRep :: a -> ChooseRep

This first thing to notice is that ChooseRep is more powerful than our simple list. It's able to perform IO actions to produce the appropriate representation. This is very useful: perhaps we showing the HTML representation of data, you need to do some expensive database lookups, whereas the JSON version doesn't need that data. You can make sure you only run the IO operations when the user actually wants HTML.

The HasReps typeclass is the real winner here. It's trivial to now define instances of HasReps that specify which mime-type they return. Some real-life examples from Yesod:

newtype RepHtml = RepHtml Content
instance HasReps RepHtml where
    chooseRep (RepHtml c) _ = return (typeHtml, c)

newtype RepJson = RepJson Content
instance HasReps RepJson where
    chooseRep (RepJson c) _ = return (typeJson, c)

data RepHtmlJson = RepHtmlJson Content Content
instance HasReps RepHtmlJson where
    chooseRep (RepHtmlJson html json) = chooseRep
        [ (typeHtml, html)
        , (typeJson, json)

Notice how that last datatype actually supports two different mime-types. You could create a type that supports XML as well if you like, or anything else. Yesod tries to only offer the most common types, so we've stuck with the HTML+JSON combination.

Coming up

In this mini-series on Yesod under-the-hood stuff, I think I'll attack user sessions next, and some of the built-in functions to help you (ab)use them properly.


comments powered by Disqus