问题描述
我正在处理一些代码,这些代码与模拟持久图形的数据库模式接口。在我详细讨论我的具体问题之前,我认为这可能有助于提供一些动机。我的模式是围绕着书籍,人物和作者角色。一本书有许多作者角色,每个角色都有一个人。但是,不必直接对图书对象进行UPDATE查询,您必须创建一本新书,并对新版本进行修改。
现在回到Haskell land。我目前正在使用一些类型的类,但重要的是我有 HasRoles
和实体
:
类HasRoles a其中
- 获取特定'a'的所有角色
getRoles :: a - > IO [作用]
类实体a其中
- 用新实体更新实体。返回新的实体。
update :: a - > a - > IO a
这是我的问题。当你更新一本书时,你需要创建一个新的书籍版本,但是 需要复制以前的书籍角色(否则你会丢失数据)。最简单的方法是:
instance实体书籍其中
更新orig newV = insertVersion V>> = copyBookRoles orig
这很好,但有些东西让我感到困扰,这就是缺乏任何保证如果是 Entity
和 HasRoles
,那么插入一个新版本将复制现有角色。我想到了2个选择:
使用更多类型
一个'解决方案'是引入 RequiresMoreWork ab
。从上面看, insertVersion
现在返回一个 HasRoles w => RequiresMoreWork w Book
。 update
需要一个 Book
,所以要从 RequiresMoreWork
值,我们可以调用 workComplete :: RequiresMoreWork()Book - > Book
。
然而,真正的问题在于,难题最重要的部分是类型签名 insertVersion
。如果这不符合不变式(例如,它没有提及需要 HasRoles
),那么它们都会再次崩溃,并且我们又回到了违反不变量的情况。
用QuickCheck证明它
将问题移出编译时间,但至少我们仍然断言不变。在这种情况下,不变量类似于:对于也是 HasRoles
实例的所有实体,插入现有值的新版本应该具有相同的角色。
我对此有点难住。在Lisp中,我会使用方法修饰符,在Perl中我会使用角色,但Haskell中有什么可以使用的?
一方面,如果某个实体是某个实体,那么它是否与HasRoles无关。您只需提供更新代码,并且该特定类型应该是正确的。
另一方面,这意味着您将再现 copyRoles
样板文件,你肯定会忘记包含它,所以这是一个合法的问题。
当你需要动态这种性质的派遣,一种选择是使用GADT来限定类上下文:
class持久化a where
update :: a - > a - > IO a
data实体a其中
EntityWithRoles ::(Persisted a,HasRoles a)=> a - >实体a
EntityNoRoles ::(Persisted a)=> a - >实体a
实例Persisted(实体a)其中
insert(EntityWithRoles orig)(EntityWithRoles newE)= do
newRoled< - copyRoles orig newE
EntityWithRoles< $>更新orig newRoled
insert(EntityNoRoles orig)(EntityNoRoles newE)= do
EntityNoRoles< $>更新orig newE
然而,考虑到你所描述的框架,而不是有一个 update
类方法,你可以有一个 save
方法,并且 update
是一个正常功能
class持久a其中
save :: a - > IO()
- data如上实体
update ::实体a - > (a - > a) - > IO(实体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
为实体的超类,并要求所有类型都具有伪函数的 HasRoles
实例(例如 getRoles _ = return []
)。如果你的大部分实体都有角色,这实际上非常方便,它是完全安全的,虽然有点不雅。
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?
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:
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.
这篇关于如何以类似于其他语言中mixin / method修饰符/特征的方式促进代码重用?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!