correct • elegant • free

A Human Type Error

I'm implementing a bookselling website in yesod. Most of the action involves two tables in the backend database. The Book table holds information about a book such as author, title, ISBN, while the Stock table holds instances of books that we have owned. Naturally they are in a one-many relationship.

The first implementation was somewhat cobbled together, as I was trying to get the site going whilst also learning my way around yesod. Handlers would make whatever database queries they needed: the code worked, but was ugly and repetitive.

On the second go around, I realised that most of the database interaction could be abstracted into a group of function that return an Item. An Item consists of a line from the Book table, and optionally a line from the Stock table (including its unique identifier, hence what persistent calls an Entity Stock):

type Item :: (Book, Maybe (Entity Stock))

A type class method itemize allows us to turn various things -- an existing Book row, an existing Stock row, or a String holding a unique key -- into an Item. Except these can all fail in various ways, so the various instances of itemize actually return (in the yesod Handler monad) a Maybe Item. Here's part of the most important instance:

instance Itemize Book where
  itemize = itemBook

itemBook :: Session -> Book -> Handler (Maybe Item)
itemBook session book =
    stkRaw <- runDB $ selectList ...
    if not $ null stkRaw
      then return $ Just $ (book, Just $ head stkRaw)
      else do
        if quickNonCore then return $ Just (book, Nothing)
          else ...

Whilst implementing this, I ran into a nasty bug, of a sort that is rare in Haskell programs, as the type system normally catches such problems. Fortunately it didn't take me very long to track it down. The cause? In itemBook above, I initially had this line:

if quickNonCore then return Nothing

You can see the problem. At a quick glance the line looks ok, and the type system is perfectly happy with it. I fixed the problem, and got on with other stuff.

But on reflection, I realised a couple of things. First, I think those nested Maybe types should have set alarm bells ringing. A problem like this was just waiting to happen.

Secondly, there are much better ways to express this in Haskell. Rather than Maybe Item, I could use this type:

data Item = Nonesuch Text | Available Book (Entity Stock)
              | Unavailable Book Text

Not only does this fix the nested Maybe problem, it gives space for adding reasons why a book doesn't exist, or is unavailable (the Text elements). And in general, it's much more Haskellier.

I suspect the conclusion is a simple one: if you have nested Maybes, there's probably a better way to do it!