何时使用types类,何时使用types

我正在重温一段我写几个月前做的组合search的代码,并且注意到有一个替代的,更简单的方法来完成我以前用types类实现的一些事情。

具体来说,我以前有一个types的searchtypes的问题 ,它有一个typess状态,行动(国家的操作types),一个初始状态,获取(动作,状态)对列表的方式以及一种testing一个国家是否解决问题的方法:

 class Problem psa where initial :: psa -> s successor :: psa -> s -> [(a,s)] goaltest :: psa -> s -> Bool 

这有点令人不满意,因为它需要MultiParameterTypeClass扩展,并且当您想要创build这个类的实例时,通常需要FlexibleInstances和可能的TypeSynonymInstances。 它也混乱你的function签名,例如

 pathToSolution :: Problem p => psa -> [(a,s)] 

我今天注意到,我可以完全摆脱这个类,并使用types来代替,如下所示

 data Problem sa { initial :: s, successor :: s -> [(a,s)], goaltest :: s -> Bool } 

这不需要任何扩展,函数签名看起来更好:

 pathToSolution :: Problem sa -> [(a,s)] 

最重要的是,我发现在重构我的代码来使用这个抽象而不是types类之后,我的行数比以前减less了15-20%。

最大的胜利在于使用types类创build抽象的代码 – 以前我必须创build新的数据结构,以复杂的方式包装旧的数据结构,然后将它们变成Problem类的实例(这需要更多的语言扩展)大量的代码行做相对简单的事情。 重构之后,我只是有一些function完成了我想要的function。

我现在正在查看其余的代码,试图find我可以用typesreplacetypes类的实例,并获得更多的胜利。

我的问题是:这种重构在什么情况下不起作用? 在什么情况下,使用types类而不是数据types实际上是更好的,以及如何提前识别这些情况,所以您不必经过昂贵的重构?

考虑types和类在同一个程序中的情况。 这个types可以是类的一个实例,但是这很简单。 更有意思的是,你可以从fromProblemClass :: (CProblem psa) => psa -> TProblem sa写一个函数。

你执行的重构大致相当于手动内嵌fromProblemClass无论你在CProblem构造一个用作CProblem实例的东西,并且让每个接受CProblem实例的函数接受TProblem

由于这个重构的唯一有趣的部分是TProblem的定义和TProblem的实现,如果你可以为任何其他类写一个类似的types和函数,你也可以重构它以完全消除这个类。

这是什么时候工作?

考虑fromProblemClass的实现。 你将基本上部分地将类的每个函数应用到实例types的值,并且在该过程中消除对p参数的引用(这是typesreplace的)。

任何重构types类的情况都是类似的。

这是什么时候适得其反?

想象一下Show的简化版本,只定义show函数。 这允许相同的重构,应用show和replace每个实例…一个String 。 显然,我们在这里已经失去了一些东西 – 也就是能够处理原始types,并将其转换为String的能力。 Show的价值在于它被定义在各种不相关的types上。

作为一个经验法则,如果作为类的实例的types具有许多不同的function,并且这些function通常与类function在相同的代码中使用,则延迟转换是有用的。 如果在单独处理types的代码和使用类的代码之间存在明显的分界线,那么转换函数可能更适合于types类,这是一个小句法的方便。 如果几乎全部通过类函数使用types,则types类可能完全是多余的。

这是不可能的?

顺便提一下,这里的重构类似于面向对象语言中的类和接口的区别。 类似地,重构不可能的types类是那些在许多OO语言中都不能直接expression的类。

更重要的是,一些你不能轻易翻译的例子,如果有的话,以这种方式:

  • 类的types参数只出现在协变位置 ,例如函数的结果types或非函数值。 这里的着名罪犯是Monoid mempty ,并return Monad

  • 在一个函数的types中多次出现的类的types参数可能不会使这个事情变得不可能,但是这会让事情变得更为复杂。 这里值得注意的罪犯包括EqOrd ,以及基本上每个数字类。

  • 对于更高级别的非平凡使用,我不知道如何确定其具体细节,但对于Monad (>>=)是一个显着的罪犯。 另一方面,class级中的p参数不是问题。

  • 多参数types类的使用是非常重要的 ,我也不确定如何在实践中使用可怕的复杂语言,与OO语言中的多重调度类似。 同样,你的class级在这里没有问题。

请注意,鉴于上述情况,对许多标准types来说,这种重构甚至是不可能的,而且对于less数几个例外情况,这种重构会适得其反。 这不是巧合。 :]

你通过应用这个重构放弃了什么?

你放弃了区分原始types的能力。 这听起来很明显,但这可能很重要 – 如果有什么情况需要控制使用哪个原始类实例types,应用此重构会失去某种程度的types安全性,您只能通过跳过在其他地方使用相同types的箍来确保运行时的不variables。

相反,如果你确实需要使各种实例types可以互换 – 你提到的复杂的包装是这个的典型症状 – 你可以通过扔掉原有的types来获得很大的收益。 通常情况下,您并不真正关心原始数据本身,而是关于如何让您使用其他数据; 因此直接使用函数logging比间接追加层更自然。

如上所述,这与OOP及其最适合的问题types密切相关,并且与ML风格语言中典型的表示问题的“另一面”相关。

你的重构与Luke Palmer的博客文章“Haskell Antipattern:Existential Typeclass”密切相关。

我认为我们可以certificate你的重构总是会起作用的。 为什么? 直觉上来说,如果某种types的Foo包含足够的信息,以便我们可以将它变成您的Problem类的一个实例,那么我们总是可以编写一个Foo -> Problem函数,将Foo的相关信息“ Foo ”到一个Problem ,需要的信息。

更正式一点,我们可以勾画出一个重构始终有效的certificate。 首先,为了设置阶段,以下代码将Problem类实例的转换定义为一个具体的CanonicalProblemtypes:

 {-# LANGUAGE MultiParamTypeClasses, FlexibleInstances #-} class Problem psa where initial :: psa -> s successor :: psa -> s -> [(a,s)] goaltest :: psa -> s -> Bool data CanonicalProblem sa = CanonicalProblem { initial' :: s, successor' :: s -> [(a,s)], goaltest' :: s -> Bool } instance Problem CanonicalProblem sa where initial = initial' successor = successor' goaltest = goaltest' canonicalize :: Problem psa => psa -> CanonicalProblem sa canonicalize p = CanonicalProblem { initial' = initial p, successor' = successor p, goaltest' = goaltest p } 

现在我们要certificate以下几点:

  1. 对于任何types的Fooinstance Problem Foo sa ,可以编写一个canonicalizeFoo :: Foo sa -> CanonicalProblem sa函数,该函数在应用于任何Foo sa时生成与canonicalize相同的结果。
  2. 可以将使用Problem类的任何函数重写为使用CanonicalProblem的等效函数。 例如,如果你已经solve :: Problem psa => psa -> r ,你可以编写一个canonicalSolve :: CanonicalProblem sa -> r ,这相当于solve . canonicalize solve . canonicalize

我只是草拟certificate。 在(1)的情况下,假设你有一个types为Foo的这个Problem实例:

 instance Problem Foo sa where initial = initialFoo successor = successorFoo goaltest = goaltestFoo 

那么给定x :: Foo sa你可以通过replace来certificate以下几点:

 -- definition of canonicalize canonicalize :: Problem psa => psa -> CanonicalProblem sa canonicalize x = CanonicalProblem { initial' = initial x, successor' = successor x, goaltest' = goaltest x } -- specialize to the Problem instance for Foo sa canonicalize :: Foo sa -> CanonicalProblem sa canonicalize x = CanonicalProblem { initial' = initialFoo x, successor' = successorFoo x, goaltest' = goaltestFoo x } 

而后者可以直接用来定义我们想要的canonicalizeFoo函数。

在(2)的情况下,对于任何函数solve :: Problem psa => psa -> r (或涉及Problem约束的类似types),以及任何types的Fooinstance Problem Foo sa

  • 定义canonicalSolve :: CanonicalProblem sa -> r'通过solve的定义并用CanonicalProblem实例定义replace出现的所有Problem方法。
  • certificate对于任何x :: Foo sasolve x等价于canonicalSolve (canonicalize x)

(2)的具体certificate需要solve或相关函数的具体定义。 一般的certificate可以采取以下两种方式之一:

  • 对具有Problem psa约束的所有types进行归纳。
  • certificate所有的Problem函数都可以用一小部分函数来表示,certificate这个子集有CanonicalProblem等价forms,并且使用它们的各种方式保留了等价性。

如果你是从面向对象的方法。 你可以把typeclass想象成java中的接口。 当你想为不同的数据types提供相同的接口时,通常使用它们,通常涉及每个数据types的具体实现。

在你的情况下没有使用typeclass的使用,它只会使你的代码复杂化。 为了更多的信息,你总是可以参考haskellwiki来更好的理解。 http://www.haskell.org/haskellwiki/OOP_vs_type_classes

一般的经验法则是:如果你怀疑你是否需要types类,那么你可能不需要它们。