在Haskell中解释类型类

Tsu*_*oto 9 c++ java oop haskell functional-programming

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

Pau*_*son 26

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

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

所以类型类"Num"用加法,减法和乘法运算符描述数值类型."Integer"类型是"Num"的实例,这意味着Integer是实现这些运算符的类型集的成员.

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

sum :: Num a => [a] -> a
Run Code Online (Sandbox Code Playgroud)

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

但是你不能写这个:

sum :: [Num] -> Num
Run Code Online (Sandbox Code Playgroud)

因为"Num"不是一种类型.

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

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

  • 在学习Haskell之前来自OO世界,我开始认为继承是一流功能的一个非常令人遗憾的借口. (13认同)
  • Functor是Monad的超类.你只是使用错误的Monad实现. (2认同)

C. *_*ann 13

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

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

对于来自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 - 提供了语言扩展,使类型类更加强大,例如多参数类型类,它允许您基于多种类型进行ad-hoc多态函数调度(类似于什么在OOP中称为"多次调度").


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

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);
}
Run Code Online (Sandbox Code Playgroud)

需要注意的是我们,除其他事项外,定义一个接口在一个泛型类型和定义在接口类型仅出现作为一个方法返回类型,Pure.不明显的是,每次使用接口名称都应该是相同的类型(即,没有混合实现接口的不同类型),但我不知道如何表达.


yai*_*chu 10

在C++/etc中,根据this/ selfimplicit参数的类型调度"虚方法" .(该方法指向对象隐含指向的函数表)

类型类的工作方式不同,可以执行"接口"可以做的所有事情.让我们从接口无法做到的简单示例开始: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
Run Code Online (Sandbox Code Playgroud)

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

这不能用C++等人的方法来完成,在这种方法中,从对象中检索函数表 - 在这里,你甚至没有相关的对象,直到read它返回之后你怎么能调用它?

与允许这种情况发生的接口的关键实现差异在于,函数表未指向对象内部,它由编译器单独传递给被调用函数.

另外,在C++/etc中,当一个人定义一个类时,他们也负责实现他们的接口.这意味着您不仅可以创建新界面并制作Intstd::vector实现它.

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

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

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

tldr:type-classes ==太棒了


Mic*_*zyk 7

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

  1. Java接口是封闭的,这意味着任何给定类实现的接口集都是在定义它的时间和地点一劳永逸地决定的;

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

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