如何以与其他语言中的mixins/method modifiers/traits类似的方式提升代码重用?

och*_*les 9 code-reuse haskell

我正在研究一些与数据库模式接口的代码,该数据库模式为持久性图形建模.在我详细讨论我的具体问题之前,我认为可能有助于提供一些动力.我的架构围绕书籍,人物和作者角色.一本书有许多作者角色,每个角色都有一个人.但是,您必须创建新书并对新版本进行修改,而不是允许对书籍对象进行直接UPDATE查询.

现在,回到Haskell的土地上.我目前正在使用几个类型类,但重要的是我有HasRolesEntity:

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
Run Code Online (Sandbox Code Playgroud)

这是我的问题.在更新图书时,您需要创建新图书版本,但需要复制以前的图书角色(否则会丢失数据).最简单的方法是:

instance Entity Book where
    update orig newV = insertVersion V >>= copyBookRoles orig
Run Code Online (Sandbox Code Playgroud)

这是好的,但有一些困扰我,这就是缺乏不变,如果事情是任何担保Entity HasRoles,然后插入一个新的版本将复制在现有的角色.我想到了两个选择:

使用更多类型

一个'解决方案'是介绍RequiresMoreWork a b.从上面开始,insertVersion现在返回一个HasRoles w => RequiresMoreWork w Book.update想要一个Book,所以为了摆脱RequiresMoreWork价值,我们可以打电话workComplete :: RequiresMoreWork () Book -> Book.

然而,真正的问题是,拼图中最重要的部分是类型签名insertVersion.如果这与不变量不匹配(例如,它没有提到需要HasRoles)那么它再次崩溃,我们又回到了违反不变量的状态.

用QuickCheck证明它

将问题移出编译时,但至少我们仍然断言不变量.在这种情况下,不变量类似于:对于也是实例的所有实体HasRoles,插入现有值的新版本应该具有相同的角色.


我有点难过这个.在Lisp中我会使用方法修饰符,在Perl中我会使用角色,但有什么我可以在Haskell中使用吗?

Joh*_*n L 3

对于我应该如何回应这个问题,我有两种想法:

这很好,但有件事让我烦恼,那就是缺乏对不变式的任何保证,即如果某物是实体且 HasRoles,则插入新版本将复制现有角色。

一方面,如果某个东西是实体,那么它是否具有角色并不重要。您只需提供更新代码,它对于该特定类型应该是正确的。

另一方面,这确实意味着您将为copyRoles每种类型复制样板,并且您当然可能会忘记包含它,因此这是一个合理的问题。

当您需要这种性质的动态分派时,一种选择是使用 GADT 来覆盖类上下文:

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
Run Code Online (Sandbox Code Playgroud)

但是,鉴于您所描述的框架,update您可以拥有一个方法,而不是一个类save方法,并且update是一个普通函数

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)
Run Code Online (Sandbox Code Playgroud)

我希望它的一些变体更容易使用。

类型类和 OOP 类之间的主要区别是类型类方法不提供任何代码重用方法。为了重用代码,您需要将共性从类型类方法中提取出来并放入函数中,就像我update在第二个示例中所做的那样。我在第一个示例中使用的另一种方法是将所有内容转换为某种常见类型 ( Entity),然后仅使用该类型。我希望具有独立功能的第二个示例update从长远来看会更简单。

还有另一种选择可能值得探索。您可以创建HasRolesEntity 的超类,并要求所有类型都具有HasRoles带有虚拟函数的实例(例如getRoles _ = return [])。如果您的大多数实体无论如何都有角色,那么这实际上使用起来非常方便并且完全安全,尽管有些不优雅。

  • 如果将`ConstraintKinds`扩展(http://blog.omega-prime.co.uk/?p=127)添加到ghc中,我认为这会以更优雅的方式解决这个问题。在那之前,我可能会采取我在上一段中建议的路径。它安全、简单而且有效。 (2认同)