How can I promote code reuse in a manner similar to mixins/method modifiers/traits in other languages?

若如初见. 提交于 2019-12-03 12:40:23

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.

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)

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!