参数化多态性与临时多态性

我想了解参数化多态性之间的关键区别,例如Java/Scala/C++语言中泛型类/函数的多态性与Haskell类型系统中的“临时”多态性。我熟悉第一种语言,但我从未使用过Haskell。

更准确地说:

  1. Java中的类型推理算法与Haskell中的类型推断有何不同?
  2. 请给我举个例子,说明可以用Java/Scala编写某些内容,但不能用Haskell编写(根据这些平台的模块化特性),反之亦然。

提前致谢。


答案 1

根据TAPL,§23.2:

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

相比之下,临时多态性允许多态值在以不同类型“查看”时表现出不同的行为。临时多态性最常见的例子是重载,它将单个函数符号与许多实现相关联;编译器(或运行时系统,取决于重载分辨率是静态的还是动态的)根据参数的类型为函数的每个应用程序选择适当的实现。

因此,如果您考虑历史的连续阶段,非通用的官方Java(又名J2SE 5.0之前,bef.sept. 2004)具有临时多态性 - 因此您可以重载方法 - 但不是参数化多态性,因此您无法编写通用方法。当然,之后你可以同时做这两件事。

相比之下,从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

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

您可以在Java中使用有界通配符执行类似的操作。但是Haskell和Java在这方面的关键区别在于,只有Haskell可以自动进行字典传递:在这两种语言中,给定,比如说和,你可以构建一个函数,将这些作为参数,并生成对类型的实例,使用,比如说,词典顺序。现在说你得到了.在 Java 中,您必须记住 和 的实例,并传递正确的实例(由 的四个应用程序组成!)才能应用于该对象。Haskell可以应用组合性,并弄清楚如何在给定地面实例和构造函数的情况下构建正确的实例(当然,这扩展到其他构造函数)。Ord Tb0b1f(b0, b1)(("hello", 2), ((3, "hi"), 5))stringintfbetweenf


现在,就类型推断而言(这可能应该是一个不同的问题),对于这两种语言来说,它是不完整的,从某种意义上说,你总是可以编写一个编译器无法确定类型的未注释程序。

  1. 对于Haskell来说,这是因为它具有不定的(又名一等)多态性,对于它,类型推断是不可判定的。请注意,在这一点上,Java仅限于一阶多态性(Scala扩展的东西)。

  2. 对于Java,这是因为它支持逆变子类型。

但是,这些语言的主要区别在于在实践中应用类型推断的程序语句的范围,以及对类型推断结果的正确性的重要性

  1. 对于 Haskell,推理适用于所有“非高度多态性”术语,并努力根据已知算法的已发布扩展返回声音结果:

    • 从本质上讲,Haskell的推理基于Hindley-Milner,一旦推断应用程序的类型,它就会给你完整的结果,类型变量(例如上面的例子中的和)只能用非多态类型实例化(我正在简化,但这本质上是你可以在Ocaml中找到的ML风格的多态性)。AB
    • 最近的 GHC 将确保只有具有非 Damas-Milner 类型的 let 绑定或 λ 抽象才需要类型注释。
    • Haskell试图在其最毛茸茸的分机(例如GADTs)上保持相对接近这个可推断的核心。无论如何,提议的扩展几乎总是出现在一篇论文中,证明扩展类型推断的正确性
  2. 对于Java,无论如何,类型推断都以一种更有限的方式应用:

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

    推理算法本质上是GJ的算法,但是在事后的想法中增加了一些通配符(请注意,我对J2SE 6.0中可能进行的更正并不了解最新)。方法上最大的概念差异在于Java的推理是局部的,从某种意义上说,表达式的推断类型仅取决于类型系统生成的约束及其子表达式的类型,而不依赖于上下文。

    请注意,关于不完整的,有时不正确的类型推断的党派路线相对悠闲。根据规格

    另请注意,类型推断不会以任何方式影响健全性。如果推断的类型是无意义的,则调用将产生类型错误。类型推断算法应被视为启发式算法,旨在在实践中很好地实现。如果它无法推断出所需的结果,则可以改用显式类型参数。


答案 2

参数化多态性意味着,我们不关心类型,我们将为任何类型实现相同的函数。例如,在Haskell中:

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

我们不关心列表元素的类型是什么,我们只关心有多少。

但是,即席多态性(也称为方法重载)意味着我们将根据参数的类型使用不同的实现。

这是Haskell中的一个例子。假设我们想要定义一个名为 的函数。makeBreakfast

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

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

我们将创建一个名为的类型类来实现该函数。的实现将根据 输入的类型而有所不同。BreakfastFoodmakeBreakfastmakeBreakfastmakeBreakfast

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"

根据John Mitchell的《Concepts in Programming Languages》

参数化多态性和重载(也称为临时多态性)之间的关键区别在于,参数多态函数使用一种算法对许多不同类型的参数进行操作,而重载函数可能对每种类型的参数使用不同的算法。