参数多态与 Ad-hoc 多态性

我想了解一些参数多态之间的关键区别,比如 Java/Scala/C + + 语言中泛型类/函数的多态性和 Haskell 类型系统中的“ ad-hoc”多态性。我对第一种语言很熟悉,但我从没用过 Haskell。

更准确地说:

  1. 类型推断算法(例如 Java)与哈斯克尔的类型推断算法有何不同?
  2. 请给我一个例子,说明这样一种情况: 有些东西可以用 Java/Scala 编写,但不能用 Haskell 编写(根据这些平台的模块化特性) ,反之亦然。

先谢谢你。

25693 次浏览

关于参数多态和 ad-hoc 多态性的意义以及它们在哈斯克尔和 Java 中的可用程度的完整讨论是冗长的,但是,你的具体问题可以更简单地解决:

类型推断的演算法(例如 Java)与哈斯克尔的类型推断有何不同?

据我所知,Java 不做类型推理,所以区别在于 Haskell 做类型推理。

请给我一个例子,说明这样一种情况: 有些东西可以用 Java/Scala 编写,但不能用 Haskell 编写(根据这些平台的模块化特性) ,反之亦然。

Haskell 可以做而 Java 不能做的一个非常简单的例子就是定义 maxBound :: Bounded a => a。我对 Java 的了解还不足以指出 Haskell 做不到的事情。

根据 TAPL,23.2:

参数多态(...) ,允许一个单一的一块 使用变量代替实际类型来“通用地”类型化的代码,以及 然后根据需要用特定类型实例化 都是统一的: 它们的所有实例的行为都是相同的

相反,Ad-hoc 多态性允许显示多态值 不同的行为时,“观看”在不同的类型。最常见的 多态性的例子是重载,它将单个 函数符号的多个实现; 编译器(或运行时系统,取决于重载解析是静态的还是动态的)为 函数,基于参数的类型。

因此,如果考虑到历史的各个阶段,非通用的官方 Java (也就是前 J2SE 5.0,bef。2004年9月)具有 ad-hoc 多态性,所以你可以使用 重载方法,但不能使用参数多态,所以你不能使用 写一个泛型方法。之后你当然可以两者兼顾。

相比之下,从 在1990年一开始,Haskell 就是参数多态的,这意味着你可以写:

swap :: (A; B) -> (B; A)
swap (x; y) = (y; x)

其中 A 和 B 是类型变量,可以实例化为 所有类型,无需假设。

但是没有提供 临时的多态性的预先存在的构造,这意味着您可以编写应用于 好几个的函数,而只能编写 不全是类型的函数。类型类是实现这一目标的一种方法。

它们允许您描述 同学们(类似于 Java 接口的东西) ,提供您希望为泛型类型实现的函数的 类型签名。然后您可以注册一些(希望是 好几个)与此类匹配的 。同时,您可以编写一个泛型方法,比如:

between :: (Ord a)  a -> a -> a -> Bool
between x y z = x ≤ y ^ y ≤ z

其中 Ord是定义函数 (_ ≤ _)的类。当使用时,(between "abc" "d" "ghi")静态解决,用于为字符串(而不是整数)选择正确的 例子-恰好在(Java 的)方法重载时。

你可以在 Java 中用 b01做类似的事情。但是 b02: 在两种语言中,给定 Ord T的两个实例,比如 b0b1,你可以构建一个函数 f,它将这两个实例作为参数,并使用字典顺序生成成对类型 (b0, b1)的实例。现在说你被给予 (("hello", 2), ((3, "hi"), 5))。在 Java 中,您必须记住 stringint的实例,并传递正确的实例(由 f的四个应用程序组成)以便对该对象应用 between。Haskell 可以应用 b03,并找出如何构建正确的实例,只要给定地面实例和 f构造函数(当然,这可以扩展到其他构造函数)。


现在,就 类型推断类型推断而言(这可能是一个不同的问题) ,对于这两种语言,它都是 不完整,也就是说,你总是可以编写一个 没有注释程序,而编译器不能确定它的类型。

  1. 对于 Haskell,这是因为它具有 不确定(也称为第一类)多态性,对于这种多态性,类型推断是不可判断的。请注意,在这一点上,Java 仅限于一阶多态性(Scala 在此基础上进行了扩展)。

  2. 对 Java 来说,这是因为它支持 逆变分型法逆变分型法

但这些语言在实际应用中主要不同于 应用类型推断的程序语句范围语言,而且在 对正确性的重视语言中的类型推理结果也不尽相同。

  1. 对于 Haskell 来说,推理适用于所有“非高度多态”的术语,并且要认真努力,根据已发表的一个著名算法的扩展来返回可靠的结果:

    • At its core, Haskell's inference is based on Hindley-Milner, which gives you complete results as soon as when infering the type of an application, 类型变量 (e.g. the A and B in the example above) can be only instantiated with 非多态性 types (I'm simplifying, but this is essentially the ML-style polymorphism you can find in e.g. Ocaml.).
    • 最近的 GHC 将确保可能需要 仅对于具有非 Damas-Milner 类型的 let 绑定或 λ- 抽象的类型注释。
    • Haskell 试图在这个不可推断的核心上保持相对接近,即使是它最毛茸茸的扩展(例如 GADT)。无论如何,提出的扩展几乎总是在论文中证明了扩展类型推理的 正确
  2. 对于 Java,类型推断无论如何都适用于 更有限的时尚:

    在 Java5发布之前,Java 中没有类型推理。根据 Java 语言文化,每个变量、方法和动态分配的对象的类型必须由程序员显式声明。当泛型(通过类型参数化的类和方法)在 Java5中引入时,语言保留了变量、方法和分配的这一要求。但是多态方法的引入(通过类型参数化)要求(i)程序员在每个多态方法调用站点提供方法类型参数,或者(ii)语言支持方法类型参数的推断。为了避免给程序员带来额外的文书负担,Java5的设计者选择执行类型推断来确定类型参数 用于多态方法调用。(来源,强调我的)

    推理算法本质上是 那个 GJ,但事后添加了 有点 一团糟通配符(注意,我没有更新在 J2SE 6.0中可能进行的更正)。在方法上最大的概念差异是 Java 的推断是 本地,在这个意义上,推断的表达式类型只依赖于类型系统生成的约束和它的子表达式的类型,而不是上下文。

    请注意,关于不完整和有时不正确的类型推断的党派路线是相对宽松的。根据 说明书:

    还要注意,类型推断不会以任何方式影响可靠性。如果推断的类型是无意义的,则调用将产生类型错误。类型推理算法应被视为一种启发式算法,旨在在实践中更好地完成。如果未能推断出所需的结果,则可以使用显式类型参数。

参数多态 意味着,我们不关心类型,我们将对任何类型实现相同的函数。例如,在哈斯克尔:

length :: [a] -> Int
length [] = 0
length (x:xs) = 1 + length xs

我们不关心列表中元素的类型,我们只关心它们的数量。

然而,Ad-hoc 多态性(即方法重载) 意味着我们将根据参数的类型使用不同的实现。

这里有一个哈斯克尔的例子,假设我们想定义一个函数,叫做 makeBreakfast

如果输入参数是 Eggs,我希望 makeBreakfast返回一条关于如何制作鸡蛋的消息。

如果输入参数是 Pancakes,我希望 makeBreakfast返回一条关于如何制作煎饼的消息。

我们将创建一个名为 BreakfastFood的类型类,它实现 makeBreakfast函数。makeBreakfast的实现将根据 makeBreakfast输入的类型而有所不同。

class BreakfastFood food where
makeBreakfast :: food -> String


instance BreakfastFood Eggs where
makeBreakfast = "First crack 'em, then fry 'em"


instance BreakfastFood Toast where
makeBreakfast = "Put bread in the toaster until brown"

根据约翰 · 米切尔的 编程语言中的概念,

参数多态和重载(即 ad-hoc 多态性)之间的关键区别在于,参数多态函数使用一种算法来操作许多不同类型的参数,而重载函数可能对每种类型的参数使用不同的算法。