首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >解释Haskell中的类型类

解释Haskell中的类型类
EN

Stack Overflow用户
提问于 2010-04-22 02:39:19
回答 6查看 2.5K关注 0票数 11

我是一名C++ / Java程序员,我在日常编程中碰巧使用的主要范式是OOP。在一些帖子中,我读到一条评论,说Type类在本质上比OOP更直观。有没有人能用简单的语言解释一下类型类的概念,这样像我这样的OOP专家就能理解它?

EN

回答 6

Stack Overflow用户

回答已采纳

发布于 2010-04-22 03:37:28

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

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

因此,类型类"Num“使用加法、减法和乘法运算符来描述数值类型。" Integer“类型是"Num”的一个实例,这意味着Integer是实现这些运算符的类型集的成员。

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

代码语言:javascript
运行
复制
sum :: Num a => [a] -> a

"=>“运算符左边的比特to表示"sum”适用于任何类型的"a“,即Num的实例。右边的位表示它接受一个"a“类型的值列表,并返回一个"a”类型的值作为结果。因此,您可以使用它对整数列表、双精度列表或复杂列表求和,因为它们都是"Num“的实例。"sum“的实现当然会使用"+”运算符,这就是为什么你需要"Num“类型的约束。

但是,您不能这样写:

代码语言:javascript
运行
复制
sum :: [Num] -> Num

因为"Num“不是一个类型。

类型和类型类之间的区别就是为什么我们不在Haskell中讨论类型的继承和后代。类型类有一种继承:您可以将一个类型类声明为另一个类型类的后代。这里的后代描述了parent所描述的类型的子集。

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

票数 29
EN

Stack Overflow用户

发布于 2010-04-22 03:22:53

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

...but,这可能没有为您澄清任何事情。

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

参数多态是指在结构类型上操作的函数,该结构类型本身是由其他类型参数化的,例如值列表。参数多态性在Haskell中几乎是常态;C#和Java将其称为“泛型”。基本上,泛型函数对特定的结构执行相同的操作,而不管类型参数是什么。

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

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

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

一些Haskell编译器--包括最流行的GHC--提供了使类型类更加强大的语言扩展,例如多参数类型类,它允许您基于多种类型进行即席多态函数分派(类似于OOP中所谓的“多重分派”)。

为了尝试给您一点它的味道,这里有一些模糊的Java/C#风格的伪代码:

代码语言:javascript
运行
复制
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);
}

请注意,我们在泛型类型上定义了一个接口,并定义了一个方法,其中接口类型只作为返回类型Pure出现。不明显的是,接口名称的每次使用都应该意味着相同的类型(即,不能混合实现接口的不同类型),但我不确定如何表达这一点。

票数 13
EN

Stack Overflow用户

发布于 2010-04-22 03:35:20

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

类型类的工作方式不同,它可以做“接口”所能做的一切,甚至更多。让我们从一个简单的接口不能做的事情开始:Haskell的Read类型类。

代码语言:javascript
运行
复制
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这样的原始类型实现适当的类型类。

随着类型类的丰富,人们倾向于编写更通用的类型,然后拥有更多可重用的函数,而且由于当类型是通用的时,他们的自由度也较小,因此甚至可能会产生较少的bug!

tldr:类型类==棒极了

票数 10
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/2685626

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档