Haskell 何时使用类型类,何时使用类型

Haskell 何时使用类型类,何时使用类型,haskell,types,typeclass,Haskell,Types,Typeclass,我在重温几个月前为进行组合搜索而编写的一段代码时,注意到有一种替代的、更简单的方法可以完成我以前通过类型类实现的任务 具体地说,我以前有一个搜索问题类型的类型类,它有一个类型为s的状态,一个类型为a的操作(对状态的操作),一个初始状态,一种获取(操作,状态)对列表的方法,以及一种测试状态是否为解决方案的方法: class Problem p s a where initial :: p s a -> s successor :: p s a -> s ->

我在重温几个月前为进行组合搜索而编写的一段代码时,注意到有一种替代的、更简单的方法可以完成我以前通过类型类实现的任务

具体地说,我以前有一个搜索问题类型的类型类,它有一个类型为
s
的状态,一个类型为
a
的操作(对状态的操作),一个初始状态,一种获取(操作,状态)对列表的方法,以及一种测试状态是否为解决方案的方法:

class Problem p s a where
    initial   :: p s a -> s
    successor :: p s a -> s -> [(a,s)]
    goaltest  :: p s a -> s -> Bool
这有点不令人满意,因为它需要MultiParameterTypeClass扩展,并且通常需要FlexibleInstances,当您想要创建此类的实例时,可能还需要TypeSynonymInstances。它也会弄乱你的函数签名,例如

pathToSolution :: Problem p => p s a -> [(a,s)]
今天我注意到我可以完全摆脱这个类,而是使用一个类型,如下所示

data Problem s a {
    initial   :: s,
    successor :: s -> [(a,s)],
    goaltest  :: s -> Bool
}
这不需要任何扩展,函数签名看起来更好:

pathToSolution :: Problem s a -> [(a,s)]
而且,最重要的是,我发现在重构代码以使用此抽象而不是类型类之后,我的行数比以前少了15-20%

最大的成功是在使用类型类创建抽象的代码中——以前我必须创建新的数据结构,以复杂的方式包装旧的数据结构,然后将它们制作成
问题
类的实例(需要更多的语言扩展)——要做一些相对简单的事情,需要大量的代码行。在重构之后,我有了几个函数,这些函数正是我想要的

我现在浏览代码的其余部分,试图找出可以用类型替换类型类的实例,并获得更多的成功


我的问题是:在什么情况下重构不起作用?在什么情况下,使用类型类实际上比使用数据类型更好?您如何提前识别这些情况,从而不必经历代价高昂的重构?

如果您来自OOP Back搁浅。您可以将TypeClass看作java中的接口。当您希望为不同的数据类型提供相同的接口时,通常会使用它们,通常涉及每个数据类型的特定数据类型实现

在您的情况下,使用typeclass是没有用的,它只会使代码过于复杂。 要了解更多信息,您可以随时参考Haskell Wiki以更好地理解。


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

考虑类型和类都存在于同一程序中的情况。该类型可以是该类的实例,但这相当简单。更有趣的是,您可以从ProblemClass::(CProblem p s a)=>p s a->TProblem s a编写一个函数

您执行的重构大致相当于在构建用作
CProblem
实例的任何地方手动内联
fromProblemClass
,并使每个接受
CProblem
实例的函数都接受
TProblem

由于此重构的唯一有趣部分是
TProblem
的定义和
fromProblemClass
的实现,因此如果您可以为任何其他类编写类似的类型和函数,您也可以对其进行重构以完全消除该类

这什么时候起作用? 考虑一下
fromProblemClass
的实现。实际上,您将部分地将类的每个函数应用于实例类型的一个值,并在此过程中消除对
p
参数的任何引用(该类型将替换该参数)

任何直接重构类型类的情况都将遵循类似的模式

什么时候会适得其反? 想象一下
Show
的简化版本,只定义了
Show
函数。这允许同样的重构,应用
show
并将每个实例替换为。。。一个
字符串
。显然,我们在这里失去了一些东西——即,处理原始类型并在不同点将它们转换为
字符串的能力。
Show
的价值在于它是在各种不相关的类型上定义的

根据经验,如果有许多不同的函数特定于作为类实例的类型,并且这些函数通常与类函数在同一代码中使用,则延迟转换非常有用。如果在单独处理类型的代码和使用该类的代码之间存在明显的分界线,那么转换函数可能更合适,因为类型类只是一个次要的语法便利。如果类型几乎完全通过类函数使用,那么类型类可能是完全多余的

什么时候不可能? 顺便说一句,这里的重构类似于OO语言中类和接口之间的区别;类似地,无法进行重构的类型类是那些在许多OO语言中根本无法直接表达的类型类

更重要的是,一些你无法用这种方式轻松翻译的例子:

  • 类的类型参数仅出现在协变位置,例如函数的结果类型或非函数值。这里值得注意的违规者是
    Monoid的
    mempty
    Monad的
    return

  • 类的类型参数在函数的类型中多次出现可能并不能真正做到这一点,但它会使问题变得非常复杂。这里值得注意的违犯者包括
    Eq
    Ord
    ,以及基本上每一个数字类

  • 更高种类的非平凡使用,规范
    {-# LANGUAGE MultiParamTypeClasses, FlexibleInstances #-}
    
    class Problem p s a where
        initial   :: p s a -> s
        successor :: p s a -> s -> [(a,s)]
        goaltest  :: p s a -> s -> Bool
    
    data CanonicalProblem s a = CanonicalProblem {
        initial'   :: s,
        successor' :: s -> [(a,s)],
        goaltest'  :: s -> Bool
    }
    
    instance Problem CanonicalProblem s a where
        initial = initial'
        successor = successor'
        goaltest = goaltest'
    
    canonicalize :: Problem p s a => p s a -> CanonicalProblem s a
    canonicalize p = CanonicalProblem {
        initial' = initial p,
        successor' = successor p,
        goaltest' = goaltest p
    }
    
    instance Problem Foo s a where
        initial = initialFoo
        successor = successorFoo
        goaltest = goaltestFoo
    
    -- definition of canonicalize
    canonicalize :: Problem p s a => p s a -> CanonicalProblem s a
    canonicalize x = CanonicalProblem {
                         initial' = initial x,
                         successor' = successor x,
                         goaltest' = goaltest x
                     }
    
    -- specialize to the Problem instance for Foo s a
    canonicalize :: Foo s a -> CanonicalProblem s a
    canonicalize x = CanonicalProblem {
                         initial' = initialFoo x,
                         successor' = successorFoo x,
                         goaltest' = goaltestFoo x
                     }