如何以类似于其他语言中的 mixins/方法修饰符/traits 的方式促进代码重用?

发布于 2024-12-02 03:33:22 字数 1449 浏览 0 评论 0原文

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

现在,回到哈斯克尔土地。我目前正在使用一些类型类,但重要的是我有 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

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

instance Entity Book where
    update orig newV = insertVersion V >>= copyBookRoles orig

这很好,但有件事让我烦恼,那就是缺乏对不变量的任何保证,即如果某物是实体并且 HasRoles,然后插入新版本将复制现有角色。我想到了 2 个选项:

使用更多类型

一个“解决方案”是引入 RequiresMoreWork a b。从上面开始,insertVersion 现在返回一个 HasRoles w =>;需要更多工作与书籍update 想要一个 Book,因此为了摆脱 RequiresMoreWork 值,我们可以调用 workComplete :: RequiresMoreWork () Book -> ;预订

但真正的问题是,这个难题中最重要的部分是 insertVersion 的类型签名。如果这与不变量不匹配(例如,它没有提到需要 HasRoles),那么一切都会再次崩溃,我们又会回到违反不变量的情况。

使用 QuickCheck 证明这一点

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


我对此有点困惑。在 Lisp 中我会使用方法修饰符,在 Perl 中我会使用角色,但是在 Haskell 中我可以使用什么吗?

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?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

任谁 2024-12-09 03:33:22

处理具体问题时,我会将角色作为类型的一部分,而不是类

数据 Rolled a = Rolled a [Role]

实例 Entity a =>实体(卷起)
其中 update (Rolled a rs) = Rolled (update a) rs

更一般地说,你可以只创建 Entity 的实例对

,我还没有达到 haskell zen,但我猜你最终应该在 Writer 或 State monad 中工作(或者他们的变压器版本)

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)

春庭雪 2024-12-09 03:33:22

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

这很好,但有一些东西让我烦恼,那就是缺乏
任何不变量的保证,如果某物是一个实体并且
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 <
gt; update orig newRoled
    insert (EntityNoRoles orig) (EntityNoRoles newE) = do
      EntityNoRoles <
gt; update orig newE

但是,考虑到您所描述的框架,您可以不使用 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)

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

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

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

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 <
gt; update orig newRoled
    insert (EntityNoRoles orig) (EntityNoRoles newE) = do
      EntityNoRoles <
gt; 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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文