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 p
I'm of two minds as to how I should respond to this:
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.
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 copyRoles
boilerplate 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:
class Persisted a where
update :: a -> a -> IO a
data Entity a where
EntityWithRoles :: (Persisted a, HasRoles a) => a -> Entity a
EntityNoRoles :: (Persisted a) => a -> Entity a
instance Persisted (Entity a) where
insert (EntityWithRoles orig) (EntityWithRoles newE) = do
newRoled <- copyRoles orig newE
EntityWithRoles <$> update orig newRoled
insert (EntityNoRoles orig) (EntityNoRoles newE) = do
EntityNoRoles <$> update orig newE
However, given the framework you've described, rather than having an update
class method, you could have a save
method, with update
being a normal function
class Persisted a where
save :: a -> IO ()
-- data Entity as above
update :: Entity a -> (a -> a) -> IO (Entity a)
update (EntityNoRoles orig) f = let newE = f orig in save newE >> return (EntityNoRoles newE)
update (EntityWithRoles orig) f = do
newRoled <- copyRoles orig (f orig)
save newRoled
return (EntityWithRoles newRoled)
I 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 update
in 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 standalone update
function, would be simpler in the long run.
There is another option that may be worth exploring. You could make HasRoles
a superclass of Entity and require that all your types have HasRoles
instances 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.