Alex Rivera | Logout

Safe modelling of relational data in Haskell

Asked 2012-02-10T20:11:23.310
40

I find it very common to want to model relational data in my functional programs. For example, when developing a web-site I may want to have the following data structure to store info about my users:

data User = User 
  { name :: String
  , birthDate :: Date
  }

Next, I want to store data about the messages users post on my site:

data Message = Message
  { user :: User
  , timestamp :: Date
  , content :: String
  }

There are multiple problems associated with this data structure:

  • We don't have any way of distinguishing users with similar names and birth dates.
  • The user data will be duplicated on serialisation/deserialisation
  • Comparing the users requires comparing their data which may be a costly operation.
  • Updates to the fields of User are fragile -- you can forget to update all the occurences of User in your data structure.

These problems are manageble while our data can be represented as a tree. For example, you can refactor like this:

data User = User
  { name :: String
  , birthDate :: Date
  , messages :: [(String, Date)] -- you get the idea
  }

However, it is possible to have your data shaped as a DAG (imagine any many-to-many relation), or even as a general graph (OK, maybe not). In this case, I tend to simulate the relational database by storing my data in Maps:

newtype Id a = Id Integer
type Table a = Map (Id a) a

This kind of works, but is unsafe and ugly for multiple reasons:

  • You are just an Id constructor call away from nonsensical lookups.
  • On lookup you get Maybe a, but often the database structurally ensures that there is a value.
  • It is clumsy.
  • It is hard to ensure referential integrity of your data.
  • Managing indices (which are very much necessary
Edit
Report

2 Answers

5

Another radically different approach to representing relational data is used by the database package haskelldb. It doesn't work quite like the types you describe in your example, but it is designed to allow a type-safe interface to SQL queries. It has tools for generating data types from a database schema and vice versa. Data types such as the ones you describe work well if you always want to work with whole rows. But they don't work in situations where you want to optimize your queries by only selecting certain columns. This is where the HaskellDB approach can be useful.

answered 2012-02-16T17:59:38.763
3

I don't have a complete solution, but I suggest taking a look at the ixset package; it provides a set type with an arbitrary number of indices that lookups can be performed with. (It's intended to be used with acid-state for persistence.)

You do still need to manually maintain a "primary key" for each table, but you could make it significantly easier in a few ways:

  1. Adding a type parameter to Id, so that, for instance, a User contains an Id User rather than just an Id. This ensures you don't mix up Ids for separate types.

  2. Making the Id type abstract, and offering a safe interface to generating new ones in some context (like a State monad that keeps track of the relevant IxSet and the current highest Id).

  3. Writing wrapper functions that let you, for example, supply a User where an Id User is expected in queries, and that enforce invariants (for example, if every Message holds a key to a valid User, it could allow you to look up the corresponding User without handling a Maybe value; the "unsafety" is contained within this helper function).

As an additional note, you don't actually need a tree structure for regular data types to work, since they can represent arbitrary graphs; however, this makes simple operations like updating a user's name impossible.

answered 2012-02-10T20:46:04.887

Your Answer