Friday, September 10, 2010

Untitled

A few months ago, there was a useful tutorial on using CouchDB with Haskell. You can find the original here.

One weakness of these DB layers is that you have to verify your data, and the APIs to the input data are usually not typesafe. As it turns out, it's incredibly easy to use type-level programming to make your DB calls typesafe.

Lets take an idealized version of a typical DB get call:

getDBUnsafe :: (JSON a) => DB -> String -> IO a
getDBUnsafe = undefined

There's two weak spots here. The first is the part where we pass in the DB key, and the second is when we use the value. The latter is more insidious than the former, because whatever it is you're using to parse your JSON, it's probably a pure function, so if you're coding in the usual expedient way, you'll have no idea why the parse is failing, when the real cause is that you're fetching from the wrong DB.

So here's the framework for a solution that uses FunctionalDependencies and MultiParamTypeClasses to impose a constraint on the type of the DB key and stored value, based on the type of the DB.

class (JSON v) => DBTy a k v | a -> k, a -> v where
getDBName :: a -> String
getKey :: a -> k -> String

getDB :: (DBTy a k v) => a -> k -> IO v
getDB db k =
getDBUnsafe (getDBName db) (getKey db k)

So how we use this? Well you just define a dummy type for a DB like such:

type UserId = Int

data Avatar
= Avatar ByteString
deriving (Eq, Show, Ord, Typeable, Data)

instance JSON Avatar where
showJSON = toJSON
readJSON = fromJSON

data AvatarsDB
= AvatarsDB String

instance DBTy AvatarsDB Int Avatar where
getDBName (AvatarsDB name) = name
getKey _ = show

 

After that, you just replace your unsafe DB calls that look like this:

v <- getDBUnsafe "avatars" userId

with this:

v <- getDB AvatarsBB userId

 

Oh and here's the stuff you need to paste at the top of all this to get it to compile.

{-# LANGUAGE FunctionalDependencies, MultiParamTypeClasses, DeriveDataTypeable #-}
module FundepsExample where

import Data.ByteString.Lazy

import Text.JSON
import Text.JSON.Generic

type DB = String

 

I know it's a pretty silly example and use case, but I was surprised that there were folks in my local Haskell meetup who hadn't seen it, so I thought I should share it. It's saved me no end of errors ever since I put it to use.

Now in practice, I've found it more useful to create a KeyStringTy class instead of using that getKey bit. As it so happens, the sort of stuff I typically use to index my bit bucket database are also the sort of stuff that I use in RPC calls from the web. It's probably less correct, but it sure was dandier to code. Watch it bite me in the ass someday.

Posted via email from lambdasquirrel's posterous