在Haskell中解释类型类

时间:2010-04-21 18:39:19

标签: java c++ oop haskell functional-programming

我是一名C ++ / Java程序员,我碰巧在日常编程中使用的主要范例是OOP。在某些线程中,我读到了一个注释,Type类本质上比OOP更直观。有人能用简单的单词解释类型类的概念,以便像我这样的OOP人能理解吗?

6 个答案:

答案 0 :(得分:26)

首先,我总是非常怀疑这个或那个程序结构更直观的说法。编程是违反直觉的,并且总是因为人们自然而然地根据具体情况而不是一般规则来思考。改变这一点需要培训和实践,也称为“学习编程”。

继续讨论问题,OO类和Haskell类型类之间的关键区别在于,在OO中,类(甚至是接口类)既是新类型的类型,也是新类型的模板(后代)。在Haskell中,类型类只是 新类型的模板。更确切地说,类型类描述了一组共享公共接口的类型,但本身并不是一个类型

因此类型类“Num”描述了带加法,减法和乘法运算符的数值类型。 “Integer”类型是“Num”的实例,这意味着Integer是实现这些运算符的类型集的成员。

所以我可以用这种类型写一个求和函数:

sum :: Num a => [a] -> a

“=>”左侧的位运算符表示“sum”适用于任何类型的“a”,它是Num的一个实例。右边的位表示它采用类型“a”的值列表,并返回单个值“a”的值作为结果。因此,您可以使用它来汇总整数列表或双打列表或复杂列表,因为它们都是“Num”的实例。 “sum”的实现当然会使用“+”运算符,这就是你需要“Num”类型约束的原因。

但你不能这样写:

sum :: [Num] -> Num

因为“Num”不是类型。

类型和类型类之间的区别是我们不讨论Haskell中类型的继承和后代的原因。 类型类的继承:您可以将一个类型类声明为另一个类型的后代。这里的后代描述了父级描述的类型的子集。

所有这一切的一个重要结果是你不能在Haskell中拥有异类列表。在“sum”示例中,您可以传递整数列表或双精度列表,但不能在同一列表中混合使用双精度数和整数。这看起来像一个棘手的限制;您如何实施旧的“汽车和卡车都是两种类型的车辆”的例子?根据您实际尝试解决的问题,有几个答案,但一般原则是您使用第一类函数显式执行间接,而不是隐式使用虚函数。

答案 1 :(得分:12)

嗯,简短的版本是:类型类是Haskell用于ad-hoc多态的类型。

......但这可能没有为你澄清任何事情。

对于来自OOP背景的人来说,多态性应该是一个熟悉的概念。然而,这里的关键点是参数 ad-hoc 多态之间的区别。

参数多态表示对结构类型进行操作的函数,结构类型本身由其他类型(例如值列表)参数化。参数多态在Haskell中几乎都是常态; C#和Java称之为“泛型”。基本上,无论类型参数是什么,泛型函数对特定结构都做同样的事情。

另一方面,

Ad-hoc多态意味着不同功能的集合,根据类型执行不同的(但在概念上相关)事物。与参数多态不同,需要为可以使用的每种可能类型单独指定ad-hoc多态函数。 Ad-hoc多态性因此是其他语言中各种功能的通用术语,例如C / C ++中的函数重载或OOP中基于类的调度多态。

Haskell类型类比其他形式的ad-hoc多态性的一个主要卖点是更强的灵活性,因为允许类型签名中的任何位置的多态性。例如,大多数语言不会根据返回类型区分重载函数;类型类可以。

许多OOP语言中的接口与Haskell的类型类有些相似 - 您指定了一组要以ad-hoc多态方式处理的函数名/签名,然后明确描述了如何使用各种类型有这些功能。 Haskell的类型类使用类似,但具有更大的灵活性:您可以为类型类函数编写任意类型的签名,用于实例选择的类型变量出现在您喜欢的任何位置,而不仅仅是正在调用方法的对象类型

一些Haskell编译器 - 包括最流行的GHC - 提供语言扩展,使类型类更加强大,例如多参数类型类,它可以让你做ad-hoc多态基于多种类型的函数调度(类似于OOP中所谓的“em>”多次调度“)。


为了尝试给你一些它的味道,这里有一些模糊的Java / C#-flavvored伪代码:

interface IApplicative<>
{
    IApplicative<T> Pure<T>(T item);
    IApplicative<U> Map<T, U>(Function<T, U> mapFunc, IApplicative<T> source);
    IApplicative<U> Apply<T, U>(IApplicative<Function<T, U>> apFunc, IApplicative<T> source);
}

interface IReducible<>
{
    U Reduce<T,U>(Function<T, U, U> reduceFunc, U seed, IReducible<T> source);
}

请注意,除其他事项外,我们还要在通用类型上定义接口,并定义一种方法,其中接口类型仅显示为返回类型,{ {1}}。不明显的是,接口名称的每次使用都应该是相同的类型(即,没有混合实现接口的不同类型),但我不知道如何表达它。

答案 2 :(得分:10)

在C ++ / etc中,根据this / self隐式参数的类型调度“虚方法”。 (该方法指向对象隐含指向的函数表中)

类型类的工作方式不同,可以执行“接口”可以做的所有事情。让我们从一个接口无法做的事情的简单示例开始:Haskell的Read类型类。

ghci> -- this is a Haskell comment, like using "//" in C++
ghci> -- and ghci is an interactive Haskell shell
ghci> 3 + read "5" -- Haskell syntax is different, in C: 3 + read("5")
8
ghci> sum (read "[3, 5]") -- [3, 5] is a list containing 3 and 5
8
ghci> -- let us find out the type of "read"
ghci> :t read
read :: (Read a) => String -> a

read的类型为(Read a) => String -> a,这意味着对于实现Read类型类的每种类型,read都可以将String转换为类型。这是基于返回类型的调度,不可能使用“接口”。

在C ++等人的方法中无法做到这一点,在这种方法中,从对象中检索函数表 - 在这里,你甚至没有相关对象,直到read返回它为止所以你怎么能调用它?

允许这种情况发生的接口的一个关键实现差异是,函数表没有指向对象内部,它由编译器单独传递给被调用函数。

此外,在C ++ / etc中,当一个人定义一个类时,他们也负责实现他们的接口。这意味着您不能仅仅发明一个新界面并让Intstd::vector实现它。

在Haskell中你可以,而不是像Ruby中的“猴子修补”那样。 Haskell有一个很好的名称间隔方案,这意味着两个类型类都可以具有相同名称的函数,并且类型仍然可以实现它们。

这允许Haskell有许多简单的类,如Eq(支持相等性检查的类型),Show(可以打印到String的类型),Read (可以从String解析的类型),Monoid(具有连接操作和空元素的类型)等等,并允许甚至像Int这样的原始类型实现适当的类型类。

随着类型类的丰富性,人们倾向于编程为更一般的类型,然后具有更多可重用的函数,并且因为当类型一般时它们也具有较少的自由度,它们甚至可以产生更少的错误!

tldr:type-classes == awesome

答案 3 :(得分:7)

除了xtofl和camccann已经在他们的优秀答案中编写的内容之外,在将Java的接口与Haskell的类型类进行比较时需要注意的一点是:

  1. Java接口已关闭,这意味着任何给定类实现的接口集都是一次性决定的,无论何时何地定义;

  2. Haskell的类型类是 open ,这意味着任何类型(或多参数类型类的类型组)都可以随时成为任何类型类的成员,因为可以为类型类定义的函数提供合适的定义。

  3. 类型类的开放性(和Clojure的协议非常相似)是一个非常有用的属性; Haskell程序员通常会通过巧妙地使用类型类来立即将它应用于涉及预先存在的类型的一系列问题。

答案 4 :(得分:3)

可以将类型类与“实现”接口的概念进行比较。如果Haskell中的某些数据类型实现了“Show”接口,那么它可以用于所有期望“Show”对象的函数。

答案 5 :(得分:1)

使用OOP,您继承了接口和实现。 Haskell类型允许将它们分开。两个完全不相关的类型都可以暴露相同的接口。

也许更重要的是,Haskell允许在“事后”添加类实现。也就是说,我可以创建一些我自己的新类型,然后去使所有标准的预定义类型成为这个类的实例。在OO语言中,无论多么有用,你[通常]都不能轻易地将新方法添加到现有类中。