Yesod in production

The most popular and fully-featured web application framework currently on the market for Haskell is Yesod. It's definitely production ready, armed with a neato Hebrew name and 2 metric tons of monad transformers; and having just ported my site to it, there's no better person on this blog than me to tell you the pros and cons of using Yesod.

A taster

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
getEntryR :: Text -> Handler Html
getEntryR e = do
    -- retrieve an entry from the DB
    entity@(Entity k entry) <- runDB . getBy404 $ UniqueSlug e

    -- redirect unauthenticated users
    authId <- maybeAuthId
    when (isNothing authId && not (entryPublished entry))
        $ permissionDenied "You can't read that."

    img <- imageOf entity
    defaultLayout $ do
        setTitle $ toHtml $ entryTitle entry
        -- render entry-specific CSS
        toWidget $(luciusFile "templates/entries.lucius")
        -- render entry-specific template
        case entryType entry of Blog   -> $(widgetFile "single-blog")
                                Code   -> $(widgetFile "single-code")
                                Design -> $(widgetFile "single-design")

(Forgive my references to Rails in the following paragraph; it's the other framework I'm most familiar with)

Although it doesn't look like it, Yesod really is a traditional MVC framework like Rails; it's just more explicit, since Haskell isn't as metaprogramming-friendly as Ruby. defaultLayout is a function that renders the default-layout.{hamlet,lucius,julius} files, plus anything else you want to pass to it. Omitting defaultLayout and simply doing something like $(widgetFile "templates/my-file.hamlet") would be similar to render :layout => false in Rails.

For those unfamiliar, $(...) is Template Haskell splicing syntax; the luciusFile, juliusFile, hamletFile, and widgetFile functions convert the relevant templates into Haskell and insert them directly into the current handler. This means your templates are compile-safe, which rocks (see The good below), and also that (in development) your app is automatically recompiled when you change them.

UniqueSlug is generated by Persistent, Yesod's database interface layer. It's a convenient data constructor that represents (obviously) a unique slug (URL part) and enforces this in the database.

I thought Template Haskell was evil!

It's a matter of preference. Personally I have no problem with it: the benefits outweigh the drawbacks in my opinion, although Yesod is usually most criticized for its heavy use of Template Haskell. Surprisingly, I hardly ever (read: never) had situations where TH templates made it more difficult to track down compile errors, so don't let that turn you off. Templates can be mixed and matched wherever you please, and even included within each other using the ^{...} syntax, which is a HUGE win for modularity.

I looked at your site's source on GitHub and Foundation.hs is gross.

Absolutely agreed, but yesod init writes virtually all of that code for you (except you'll need to customize the bits that specify what authentication methods you want).

The good

Compile-time-safe templates

Since the templates you write are actually inserted into your handler code when compiled, type errors, unbound names, and so on are caught much earlier in the process than they might be in something like Haml (although that's a problem symptomatic of using dynamic languages in general, not specifically in this use case). Also, the syntax is quite friendly and very intuitive, and you can specify exactly how much whitespace you want—you don't realize how difficult Haml makes this until you've used Hamlet.

Apart from HTML, this also means that you get type-safe mixins in your CSS and type-safe asset URLs in your Javascript.

Models and migrations

I cannot express how much I love this feature. You get your models and your HABTM relations, typechecked for free—these constraints will also be expressed in the DB to the best of the adapter's ability, so you can't accidentally destroy a model that something else is expecting to exist. Every time you change the models file, your database will be auto-migrated—unless it contains destructive changes, in which case Persistent will tell you to manually intervene. Awesome.

Batteries included

  • Built in support for BrowserID, Gmail authentication, OpenID, looking up users by a hashed password + salt, etc.
  • Rest APIs are easy. Just provideRep any content type you want.
  • The templating languages don't even suck.
  • Built in internationalization support. I can only assume this is good since I don't use it myself, but it looked easy to implement based on the blog post (typesafe, too.)
  • But...

The bad

Not all the batteries are included

Since Yesod's ecosystem is so much smaller than Django's or Rails' at the moment, there are a lot of things you can take for granted with Rails that you might even have to write yourself for Yesod. There's no Yesod version of paperclip, which I had to write myself (it'll be on Hackage one of these days!) and the markdown package needed some serious work before it was all production-ready ([1], and see The meh below).

Azathoth's error messages

If you want to effectively debug some of the errors you will come across when writing your Yesod app, you'll need to either have a deep familiarity with yesod-core or be a wizard. You've got your HandlerT for handlers, your PersistMonadBackend for code that interacts with your database, and MForms. You might want to take this opportunity to spend some time reading through the yesod-core source; or, if you're me, you just add runDB everywhere.

What I generally recommend for this, actually, is something like

1
data Foo = Foo

and then adding myFunc :: Foo wherever you encounter a difficult function to write; GHC will tell you what it expects, and you can gradually narrow down the correct type.

Cthulhu's type signatures

This is what GHC suggested for one of my helper functions:

1
2
3
4
5
6
7
8
9
imageOf :: forall site.
           (YesodPersist site,
            PersistStore (YesodPersistBackend site (HandlerT site IO)),
            PersistMonadBackend (YesodPersistBackend site (HandlerT site IO))
            ~ SqlBackend) =>
           Entity Entry -> HandlerT site IO (Maybe Image)
imageOf entity = case entryImage (entityVal entity) of
                      Just imgId -> runDB $ get imgId
                      Nothing -> return Nothing

Note: I've gotten a few comments about this section, and I want to clarify that I know the type signature can be specialized to the one-liner Entity Entry -> Handler (Maybe Image). My point was more that these simplifications are not always obvious, and people who are not experienced with Yesod are bound to find such huge suggested type signatures intimidating.

Deployment is easier, but slower

Now that I'm using a compiled language, I have a directory full of config files, one with some static images, and an 85 megabyte binary. Nobody could contest that this is simpler than deploying with git (it's basically tar → scp → untar → service restart), but I just have to clobber the old binary each time, which is a bit inefficient.

Another problem I've encountered here is that since the site is now a dynamically linked binary instead of 30 files, it's no longer platform-agnostic like Ruby; I need to maintain a VM with the same flavor and version of Linux that my VPS has, and I need to provision the VPS the same way. Chef makes this quick once it's configured properly, which is a huge pain in the ass.[2] To bring up the dev environment, or even to deploy, I have to spin up the VM.

The meh

Coming from a Ruby background, I'm used to being able to monkeypatch in whatever functionality I want into whatever module whenever. A prime example is this file, where my Rails syntax highlighter was implemented: parse the entry contents and replace all pres that have a lang attribute with the highlighted version. Needless to say this is a completely non-serviceable route in Haskell. You could, I suppose, parse your rendered Markdown and replace blocks that way—this isn't only hard, but dangerous; blaze-html uses existential types, which necessitates some unsafeCoerce if you're recursing on HTML nodes. I pretty quickly realized there was no way this would work for me.

I ended up forking snoyberg/markdown and adding markdown-kate support, which was merged into the markdown package in version 0.1.3. This is definitely a better solution.

Summary

The good stuff is great. My server resource usage is down, the site is quicker, and deployment is crazy easy. But! trying to debug the inevitable type errors can be a nightmare, even if you're comfortable with Haskell. If you're planning on writing a big project with Yesod, I'd recommend joining #haskell and #yesod on Freenode, where you're bound to find someone who's willing to help.


[1] I wrote this footnote implementation for the markdown package (it's not a standard Markdown feature). It was just added in 0.1.6. Get involved in open source! It's awesome!

[2] This is exacerbated by the fact that CentOS, Debian and friends are bound to have old versions of libraries in the official repos. I've noticed that people writing Haskell libraries tend to like using bleeding-edge third-party libs; for example, the imagemagick package won't build with the (older) imagemagick that yum will provide you. The cookbooks I'm using (and wrote) are in joelt.io's repository.

Filed in Haskell, Yesod on Jul 16, 2013.