I’m working on some code that interfaces to a database schema that models a persistent graph. Before I go into the details of my specific question, I thought it might help to provide some motivation. My schema is around books, people and author roles. A book has many author roles, where each role has a person. However, instead of allowing direct UPDATE queries on book objects, you must create a new book, and make modifications to the new version.
Now, back to Haskell land. I am currently working with a few type classes, but importantly I have HasRoles and Entity:
class HasRoles a where
-- Get all roles for a specific 'a'
getRoles :: a -> IO [Role]
class Entity a where
-- Update an entity with a new entity. Return the new entity.
update :: a -> a -> IO a
Here comes my problem. When you are updating a book, you need to create a new book version but you also need to copy over the previous books roles (otherwise you lose data). The simplest way to do this is:
instance Entity Book where
update orig newV = insertVersion V >>= copyBookRoles orig
This is fine, but there’s something that bugs me, and that’s the lack of any guarantee of the invariant that if something is an Entity and HasRoles, then inserting a new version will copy over the existing roles. I have thought of 2 options:
Use More Types
One ‘solution’ is to introduce the RequiresMoreWork a b. Going from the above, insertVersion now returns a HasRoles w => RequiresMoreWork w Book. update wants a Book, so to get out of the RequiresMoreWork value, we could call workComplete :: RequiresMoreWork () Book -> Book .
The real problem with this though, is that the most important piece of the puzzle is the type signature of insertVersion. If this doesn’t match the invariants (for example, it made no mention of needing HasRoles) then it all falls apart again, and we’re back to violating an invariant.
Prove it with QuickCheck
Moves the problem out of compile time, but at least we’re still asserting the invariant. In this case, the invariant is something like: for all entities that are also instances of HasRoles, inserting a new version of an existing value should have the same roles.
I’m a bit stumped on this. In Lisp I’d use method modifiers, in Perl I’d use roles, but is there anything I can use in Haskell?
I’m of two minds as to how I should respond to this:
One the one hand, if something is an Entity, it doesn’t matter if it HasRoles or not. You simply provide the update code, and it should be correct for that specific type.
On the other, this does mean that you’ll be reproducing the
copyRolesboilerplate for each of your types and you certainly could forget to include it, so it’s a legitimate problem.When you require dynamic dispatch of this nature, one option is to use a GADT to scope over the class context:
However, given the framework you’ve described, rather than having an
updateclass method, you could have asavemethod, withupdatebeing a normal functionI would expect some variation of this to be much simpler to work with.
A major difference between type classes and OOP classes is that type class methods don’t provide any means of code re-use. In order to re-use code, you need to pull the commonalities out of type class methods and into functions, as I did with
updatein the second example. An alternative, which I used in the first example, is to convert everything into some common type (Entity) and then only work with that type. I expect the second example, with a standaloneupdatefunction, would be simpler in the long run.There is another option that may be worth exploring. You could make
HasRolesa superclass of Entity and require that all your types haveHasRolesinstances with dummy functions (e.g.getRoles _ = return []). If most of your entities would have roles anyway, this is actually pretty convenient to work with and it’s completely safe, although somewhat inelegant.