如何以类似于其他语言中的 mixins/方法修饰符/traits 的方式促进代码重用?
我正在编写一些与数据库模式接口的代码,该数据库模式对持久图进行建模。在详细讨论我的具体问题之前,我认为这可能有助于提供一些动力。我的架构围绕着书籍、人物和作者角色。一本书有许多作者角色,每个角色都有一个人。但是,您必须创建一本新书并对新版本进行修改,而不是允许对书籍对象进行直接 UPDATE 查询。
现在,回到哈斯克尔土地。我目前正在使用一些类型类,但重要的是我有 HasRoles
和 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
这就是我的问题。当您更新书籍时,您需要创建新的书籍版本,但您还需要复制以前的书籍角色(否则您会丢失数据)。最简单的方法是:
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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
处理具体问题时,我会将角色作为类型的一部分,而不是类
数据 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)
对于如何回应这个问题,我有两种想法:
一方面,如果某个东西是实体,那么它是否具有角色并不重要。您只需提供更新代码,它对于该特定类型应该是正确的。
另一方面,这确实意味着您将为每种类型复制
copyRoles
样板,并且您当然可能会忘记包含它,因此这是一个合理的问题。当您需要这种性质的动态分派时,一种选择是使用 GADT 来覆盖类上下文:
但是,考虑到您所描述的框架,您可以不使用
update
类方法,有一个save
方法,其中update
是一个普通函数,我希望它的一些变体更容易使用。
类型类和 OOP 类之间的主要区别是类型类方法不提供任何代码重用方法。为了重用代码,您需要将共性从类型类方法中提取出来并放入函数中,就像我在第二个示例中使用
update
所做的那样。我在第一个示例中使用的另一种方法是将所有内容转换为某种通用类型 (Entity
),然后仅使用该类型。我希望第二个示例具有独立的 update 函数,从长远来看会更简单。还有另一种选择可能值得探索。您可以将
HasRoles
设为 Entity 的超类,并要求所有类型都具有带有虚拟函数的HasRoles
实例(例如getRoles _ = return []
) 。如果您的大多数实体无论如何都有角色,那么这实际上使用起来非常方便并且完全安全,尽管有些不优雅。I'm of two minds as to how I should respond to this:
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:
However, given the framework you've described, rather than having an
update
class method, you could have asave
method, withupdate
being a normal functionI 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 standaloneupdate
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 haveHasRoles
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.