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?
Dealing in the specific, I would make the roles part of a type instead of an class
data Rolled a = Rolled a [Role]
instance Entity a => Entity (Rolled a) where update (Rolled a rs) = Rolled (update a) rs
More generally, you could just make pairs instances of Entity
I haven't reached haskell zen, but I would guess you should end up working in the Writer or State monad (or their transformer versions)
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.
精彩评论