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

前端 未结 1 1151
慢半拍i
慢半拍i 2021-02-15 08:57

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

相关标签:
1条回答
  • 2021-02-15 09:21

    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.

    0 讨论(0)
提交回复
热议问题