【问题标题】:Parametric polymorphism vs Ad-hoc polymorphism参数多态性 vs Ad-hoc 多态性
【发布时间】:2011-07-18 08:05:26
【问题描述】:

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

更准确地说:

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

提前致谢。

【问题讨论】:

  • 参数多态可以忽略被操作的类型,例如reverse :: [a] -> [a]就是这样一种类型。 ad-hoc 多态函数sort :: Ord a => [a] -> [a] 需要附加信息,由类约束 Ord a 引导。我不确定“ad-hoc”的词源推导,但后一种形式的多态性不是普通 lambda 演算的特征。
  • @Eliah Java 不需要为泛型类明确指定类型。例如,List<T> filter(Function<T>, List<T>)T 是完全不可知的。您的示例是参数多态性和子类型多态性的混合,这会产生协方差和逆变换。这是临时多态性的独立野兽。
  • @Eliah 是的。你可以在心里把Read a => 之类的东西翻译成<? extends Read> 之类的东西(我不太确定,我已经很久没有做任何Java了)
  • 只是出于好奇:为什么要将 Java 的参数多态性与 Haskell 的 ad-hoc 多态性进行比较?将 Java 的参数多态性与 Haskell 的参数多态性以及 Java 的 ad-hoc 多态性与 Haskell 的 ad-hoc 多态性进行比较不是更有意义吗?
  • stackoverflow.com/questions/5671303/… 看到这个较早的答案

标签: java haskell types polymorphism type-inference


【解决方案1】:

根据TAPL,§23.2:

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

Ad-hoc 多态性,相比之下,允许多态值显示 在不同类型“查看”时的不同行为。最普遍的 ad-hoc 多态性的例子是重载,它将单个 具有许多实现的功能符号;编译器(或运行时系统,取决于重载决议是静态的还是动态的)为每个应用程序选择适当的实现 函数,基于参数的类型。

因此,如果您考虑历史的连续阶段,非泛型官方 Java(又名 pre-J2SE 5.0,2004 年 9 月之前)具有临时多态性 - 所以你可以 overload a method - 但不是参数多态性,所以你不能write a generic method。之后你当然可以两者都做。

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

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

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

但没有预先存在的构造提供 ad-hoc 多态性,它旨在让您编写适用于几个并非全部的函数> 类型。类型类被实现为实现这一目标的一种方式。

它们让您描述一个(类似于Java 接口的东西),给出您希望为您的泛型类型实现的函数的类型签名。然后你可以注册一些(希望是几个实例匹配这个类。同时,您可以编写一个泛型方法,例如:

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

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

您可以使用bounded wildcards 在 Java 中执行类似的操作。但是Haskell 和 Java 在这方面的主要区别在于只有 Haskell 可以自动进行字典传递:在这两种语言中,给定 Ord T 的两个实例,比如 b0b1,你可以构建一个函数f,将这些作为参数并使用字典顺序生成对类型(b0, b1) 的实例。现在说你得到了(("hello", 2), ((3, "hi"), 5))。在 Java 中,您必须记住 stringint 的实例,并传递正确的实例(由 f 的四个应用程序组成!)以便将 between 应用于该对象。 Haskell 可以应用compositionality,并找出如何在只给定地面实例和f 构造函数的情况下构建正确的实例(当然,这扩展到其他构造函数)。


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

  1. 对于 Haskell,这是因为它具有 impredicative(又名一流)多态性,对于这种多态性,类型推断是无法确定的。请注意,在这一点上,Java 仅限于一阶多态(Scala 扩展的东西)。

  2. 对于Java,这是因为它支持contravariant subtyping

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

  1. 对于 Haskell,推理适用于所有“非高度多态”的术语,并根据已发布的著名算法扩展做出认真的努力来返回正确的结果:

    • Haskell 的推理的核心是基于Hindley-Milner,它在推断应用程序的类型时立即为您提供完整的结果,类型变量(例如A 和@987654356上面示例中的 @)只能用 非多态 类型实例化(我正在简化,但这本质上是您可以在例如 Ocaml 中找到的 ML 样式的多态。)。
    • 最近的 GHC 将确保可能需要类型注释 only for a let-binding or λ-abstraction that has a non-Damas-Milner type
    • Haskell 已尝试在其最毛茸茸的扩展(例如GADTs)中保持相对接近这个可推断的核心。无论如何,提议的扩展几乎总是出现在一篇带有扩展类型推断的正确性证明的论文中。
  2. 对于 Java,类型推断以更有限的方式应用无论如何:

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

    inference algorithm 本质上是that of GJ,但在事后添加了一个somewhat kludgy 通配符(请注意,我没有及时了解J2SE 6.0 中可能进行的更正)。方法的巨大概念差异在于 Java 的推理是 local,即表达式的推断类型仅取决于从类型系统生成的约束及其子表达式的类型,但不在上下文中。

    请注意,关于不完整且有时不正确的类型推断的党派路线相对滞后。根据the spec

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

【讨论】:

    【解决方案2】:

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

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

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

    然而,

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

    这是 Haskell 中的一个示例。假设我们要定义一个名为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"
    

    根据 John Mitchell 的 Concepts in Programming Languages

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

    【讨论】:

      【解决方案3】:

      关于参数多态性和临时多态性的含义以及它们在 Haskell 和 Java 中的可用程度的完整讨论有点冗长;但是,您的具体问题可以更简单地解决:

      类型推断的算法如何Java 中的类型推断与 Haskell 中的类型推断有何不同?

      据我所知,Java 不进行类型推断。所以区别在于 Haskell 是这样做的。

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

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

      【讨论】:

      猜你喜欢
      • 2016-03-19
      • 2020-12-02
      • 1970-01-01
      • 2015-08-31
      • 1970-01-01
      • 2010-09-14
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多