【问题标题】:Type erasure in Haskell?在 Haskell 中键入擦除?
【发布时间】:2015-09-28 15:26:14
【问题描述】:

当我看到这段话时,我正在阅读 Haskell 上的 lecture note

这个“不关心”就是参数多态中的“参数”的意思。所有 Haskell 函数的类型参数必须是参数化的;这些功能不得关心或根据这些参数的选择做出决定。当 a 是 Int 时,函数不能做一件事,而当 a 是 Bool 时,函数不能做另一件事。 Haskell 根本没有提供编写这样一个操作的工具。语言的这种属性称为参数性。

参数化有许多深刻而深刻的后果。一个后果是称为类型擦除的东西。因为一个正在运行的 Haskell 程序永远不能根据类型信息做出决定,所以在编译期间可以删除所有类型信息。尽管在编写 Haskell 代码时类型非常重要,但在运行 Haskell 代码时它们完全无关紧要。与需要在运行时保留类型的其他语言(例如 Python)相比,此属性为 Haskell 提供了巨大的速度提升。 (类型擦除并不是让 Haskell 更快的唯一因素,但 Haskell 的时钟有时比 Python 快 20 倍。)

我不明白“所有 Haskell 函数”如何参数化? Haskell中的类型不是显式/静态的吗?另外我真的不明白类型擦除如何改善 编译时间 运行时?

对不起,如果这些问题真的很基础,我是 Haskell 的新手。

编辑:

还有一个问题:为什么作者说“尽管类型在编写 Haskell 代码时很重要,但在运行 Haskell 代码时却完全无关紧要”?

【问题讨论】:

  • 这句话从未声称“类型擦除提高了编译时间”,只是说它有助于改善 运行时
  • 函数是参数化的,如果你有一个类型参数a,你永远不能检查a并在运行时根据它的类型做出决定。因此,您可以确定像 map 这样的函数对所有输入列表的行为一致,因此您无需在运行时维护列表元素类型。将此与 C# 之类的语言进行比较,C# 允许您在运行时通过反射获取真正的列表参数类型。

标签: haskell types


【解决方案1】:

我不明白的是“所有 Haskell 函数”参数如何?

并不是说所有 Haskell 函数都是参数化的,而是说:

所有 Haskell 函数都必须是参数在它们的类型参数中

Haskell 函数不需要任何类型参数。

还有一个问题:为什么作者说“尽管类型在编写 Haskell 代码时很重要,但在运行 Haskell 代码时却完全无关紧要”?

与动态类型语言不同,您需要在运行时检查(例如)两个事物是否是数字,然后再尝试将它们相加,您正在运行的 Haskell 程序知道,如果您尝试将它们相加,那么它们必须是数字,因为编译器事先确定了它。

Haskell 中的类型不是显式/静态的吗?

Haskell 中的类型通常可以推断出来,在这种情况下,它们不需要显式。但是你是对的,它们是静态的,这就是为什么它们在运行时并不重要,因为静态意味着编译器在你的程序执行之前确保一切都具有它应该的类型。

【讨论】:

    【解决方案2】:

    可以在 Haskell 中擦除类型,因为表达式的类型要么在编译时知道(如 True),要么在运行时不重要(如 [])。

    但有一点需要注意,它假定所有值都具有某种统一的表示形式。大多数 Haskell 实现对所有内容都使用指针,因此指针指向的实际类型无关紧要(垃圾收集器除外),但您可以想象使用非统一表示的 Haskell 实现,然后是一些类型信息必须保留。

    【讨论】:

      【解决方案3】:

      其他人已经回答了,但也许一些例子可以提供帮助。

      例如,Python 会在运行时保留类型信息:

      >>> def f(x):
      ...   if type(x)==type(0):
      ...     return (x+1,x)
      ...   else:
      ...     return (x,x)
      ... 
      >>> f("hello")
      ('hello', 'hello')
      >>> f(10)
      (11, 10)
      

      上面的函数,给定任何参数x 返回一对(x,x)exceptxint 类型时。该函数在运行时测试该类型,如果发现xint,它会以特殊方式运行,而是返回(x+1, x)

      要实现上述目标,Python 运行时必须跟踪类型。也就是说,当我们这样做时

      >>> x = 5
      

      Python 不能只将5 的字节表示形式存储在内存中。它还需要用类型标记int 标记该表示,以便当我们执行type(x) 时,可以恢复该标记。

      此外,在进行x+1之类的任何操作之前,Python 需要检查类型标签以确保我们确实在处理ints。例如,如果xstring,Python 将引发异常。

      Java 等静态检查语言在运行时不需要此类检查。例如,当我们运行时

      SomeClass x = new SomeClass(42);
      x.foo();
      

      编译器已经在编译时检查了foo 确实有一个方法x,所以没有必要再这样做了。原则上,这可以提高性能。 (实际上,JVM 在类加载时会进行一些运行时检查,但为了简单起见,我们忽略这些)

      尽管如此,Java 像 Python 一样存储类型标签,因为它有一个 type(-) 类似:

      if (x instanceof SomeClass) { ...
      

      因此,Java 允许编写在某些类型上表现“特殊”的函数。

      // this is a "generic" function, using a type parameter A
      <A> A foo(A x) {
         if (x instanceof B) {  // B is some arbitrary class
            B b = (B) x;
            return (A) new B(b.get()+1);
         } else {
            return x;
         }
      }
      

      上述函数foo() 只返回它的参数,除非它是B 类型,而是为其创建一个新对象。这是使用instanceof 的结果,它要求每个对象在运行时都携带一个标签。

      说实话,这样的标签已经存在,可以实现虚拟方法,所以它不会花费更多。然而,instanceof 的存在使得在类型上导致上述非统一行为成为可能——某些类型可以以不同方式处理。

      Haskell 没有这样的 type/instanceof 运算符。具有类型的参数 Haskell 函数

      foo :: a -> (a,a)
      

      必须在所有类型上都以相同的方式表现。没有办法引起一些“特殊”行为。具体来说,foo x必须返回(x,x),我们只要看上面的类型注解就可以看出这一点。为了强调这一点,没有必要查看代码(!!)来证明这样的属性。这就是 parametricity 从上面的类型中确保的。

      【讨论】:

      • Java 示例对我来说似乎不是很好。首先,调用foo 通常确实 涉及运行时类型检查,因为JVM 必须确定foo 方法是否已在SomeClass 的某些子类中被覆盖。在第二个中,转换为A 是三重错误:您不能转换为类型参数的类型(因为擦除);从来没有任何使用强制构造函数调用,因为它的类型是完全已知的;无论如何都不需要强制转换,因为B 在这种情况下显然是A 的子类。
      • @amalloy 1) 我不完全确定,但我认为访问 vtable 并调用指向那里的方法不需要检查任何东西,只是盲目地遵循指针; 2) 尝试在没有(A) 强制转换的情况下编译代码:编译器正确地抱怨B 不是通用A 的子类型。 (不过,在运行时有一个关于未经检查的强制转换的警告)
      • 我猜关于 (1) 的公平点。您对 (2) 的说法是正确的,但实际上并不支持您的答案:这种转换根本不会在运行时发生;它编译为零字节码操作(或者,好吧,可能是转换为 Object,这基本上是一个无操作)。在编译时需要它来使编译器满意,但在运行时不做任何事情。
      • @chi,Java 是那些为了自身利益而进行过多反思的语言之一。我对 Java 不太了解,但我知道的一件事是,它的类型并没有告诉编译器它需要知道的所有内容,即使它们看起来像。
      • @amalloy 是的,我同意 - 我说的是在运行时完成的 instanceof 检查。或许我应该修改这句话。
      【解决方案4】:

      动态类型语言的实现通常需要将类型信息与每个值一起存储在内存中。这对于只有几种类型并且可以用一些标记位合理地识别它们的类 Lisp 语言来说还不错(尽管这种有限的类型会导致其他效率问题)。对于具有多种类型的语言来说,情况要糟糕得多。 Haskell 允许您将类型信息带到运行时,但它迫使您明确说明它,因此您可以注意成本。例如,将上下文Typeable a 添加到一个类型会在运行时提供一个具有该类型访问权限的值到a 类型的表示。更巧妙的是,typeclass 实例字典通常在编译时被专门化,但在足够多态或复杂的情况下,可能会在运行时继续存在。在像明显被放弃的 JHC 这样的编译器中,以及尚未启动的编译器 THC 的一种可能的可能性中,这可能导致某些类型信息以指针标记的形式泄漏到运行时。但这些情况很容易识别,而且很少会导致严重的性能问题。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2014-01-03
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多