为什么编译器不能在Haskell中为我们处理newtype?

我知道newtype在编译时擦除类型构造函数作为优化,因此newtype Foo = Foo Int结果只是一个Int. 换句话说,我不是在问这个问题。我的问题不是关于什么newtype

相反,我试图理解为什么编译器在看到单值data构造函数时不能简单地应用这种优化本身。当我使用 时hlint,它足够聪明地告诉我单值data构造函数应该是newtype. (我从来没有犯过这个错误,但尝试过看看会发生什么。我的怀疑得到了证实。)

一个反对意见可能是,如果没有newtype,我们就不能使用GeneralizedNewTypeDeriving和其他这样的扩展。但这很容易解决。如果我们说……

data Foo m a b = Foo a (m b) deriving (Functor, Applicative, Monad)

编译器可以直接告诉我们我们的愚蠢行为。

newtype当编译器总是可以自己解决时,为什么我们需要?

回答

看起来似乎有道理,它newtype主要是作为程序员提供的注释来执行优化的,编译器太愚蠢而无法自己弄清楚,有点像registerC 中的关键字。

然而,在 Haskell 中,newtype它不仅仅是编译器的建议注释;它实际上具有语义后果。类型:

newtype Foo = Foo Int
data Bar = Bar Int

声明两个非同构类型。具体来说,Foo undefinedandundefined :: Foo是等价的,而 whileBar undefinedundefined :: Bar不是等价的,结果是:

Foo undefined `seq` "not okay"    -- is an exception
Bar undefined `seq` "okay"        -- is "okay"

case undefined of Foo n -> "okay"       -- is okay
case undefined of Bar n -> "not okay"   -- is an exception

正如其他人所指出的,如果您使该data领域变得严格:

data Baz = Baz !Int

并注意只使用无可辩驳的模式匹配,然后Baz就像newtype Foo

Baz undefined `seq` "not okay"         -- exception, like Foo
case undefined of ~(Baz n) -> "okay"   -- is "okay", like Foo

换句话说,如果我祖母有轮子,她就是一辆自行车!

那么,为什么编译器在看到单值数据构造函数时不能简单地应用这种优化呢?嗯,它不能不改变程序语义的情况下进行一般的优化,所以它首先需要证明如果data在其领域中严格限制特定的任意、单构造函数、单字段类型,则语义不变。无可辩驳地匹配而不是严格匹配。由于这取决于类型值的实际使用方式,因此对于模块导出的数据类型可能很难做到,尤其是在函数调用边界处,但现有的专门化、内联、严格性分析和拆箱优化机制通常会执行在自包含代码块中进行等效优化,因此您可以获得newtype即使你data不小心使用了一个类型。但总的来说,编译器解决这个问题似乎太难了,所以记住newtype事情的负担就落在了程序员身上。

这就引出了一个显而易见的问题——为什么我们不能改变语义,使它们等价;为什么的语义newtypedata放在第一位不同?

嗯,newtype语义的原因似乎很明显。由于newtype优化的性质(在编译时擦除类型和构造函数),不可能 - 或者至少非常困难 -在编译时分别表示Foo undefinedundefined :: Foo解释这两个值的等价性. 因此,当只有一个可能的构造函数并且该构造函数不可能不存在时,无可辩驳的匹配是一种明显的进一步优化(或者至少不可能区分构造函数的存在和不存在,因为这是可能发生的唯一情况是在区分Foo undefinedundefined :: Foo,我们已经说过在编译代码中无法区分)。

一个构造函数、一个字段data类型(在没有严格注释和无可辩驳的匹配的情况下)的语义的原因可能不太明显。然而,这些语义与构造函数和/或字段计数不为 1 的数据类型完全一致,而newtype语义会在类型的这种特殊情况data与所有其他情况之间引入任意不一致。

由于datanewtype类型之间的这种历史区别,许多后续扩展对它们进行了不同的处理,进一步巩固了不同的语义。您提到GeneralizedNewTypeDeriving哪个适用于newtypes 但不适用于单构造函数、单字段data类型。在用于安全强制(即Data.Coerce)和的表示等价的计算、DerivingVia存在量化或更一般的 GADT、UNPACKpragma 等的使用方面存在进一步差异。在泛型中表示类型的方式也存在一些差异,尽管现在我更仔细地观察它们,它们看起来很肤浅。

即使newtypes 是一个不必要的历史错误,可以用特殊外壳某些data类型来代替,但现在将精灵放回瓶子里也有点晚了。

此外,newtype在我看来,s 并不是对现有设施的不必要重复。对我来说,datanewtype类型在概念上是完全不同的。一data类是代数,加总的产品类型,这只是巧合,代数类型的特定特殊情况恰好有一个构造函数和一个场等结束是(几乎)同构于字段类型。相比之下, anewtype从一开始就打算成为现有类型的同构,基本上是一个带有额外包装器的类型别名,以在类型级别区分它并允许我们传递单独的类型构造函数、附加实例等.

  • I don't think it's nearly as hard as you suggest. `data Bar a = Bar !a` can be translated to `newtype Bar a = Bar_ a` with `pattern Bar :: a -> Bar a; pattern Bar a <- Bar_ !a where Bar = Bar_`. Obviously, pattern synonyms are new, but the compiler's always pulled similar-enough tricks for worker-wrapper.

回答

这是一个很好的问题。语义上,

newtype Foo = Foo Int

等同于

data Foo' = Foo !Int

除了前者的模式匹配是惰性的,而后者是严格的。所以编译器当然可以将它们编译成相同的,并调整模式匹配的编译以保持语义正确。

对于您所描述的类型,这种优化在实践中并不是那么重要,因为用户可以根据需要使用newtype和添加seqs 或 bang 模式。对于存在量化的类型和 GADT,它会变得更有用。也就是说,我们希望获得更紧凑的表示,例如

data Baz a b where
  Baz :: !a -> Baz a Bool

data Quux where
  Quux :: !a -> Quux

但 GHC 目前不提供任何此类优化,在这些情况下这样做会有些棘手。


回答

newtype当编译器总是可以自己解决时,为什么我们需要?

它不能。datanewtype具有不同的语义:data添加了额外的间接级别,同时newtype具有与其包装类型完全相同的表示形式,并且始终使用惰性模式匹配,同时您可以data使用严格注释(!或类似的编译指示StrictData)选择是使惰性还是严格。

同样,编译器并不总是确定何时data可以用newtype. 严格性分析允许它保守地确定何时可以消除围绕始终要评估的事物的不必要的懒惰;在这种情况下,它可以在本地有效地去除data包装器。当去除多余的拳击与上一盒装的数字类型像操作链拆箱GHC有类似的功能,所以它可以完成大部分的计算上更有效地拆箱。但总的来说(也就是说,没有全局优化)它无法知道某些代码是否依赖于那个 thunk 的存在。IntInt#

所以 HLint 提供这个作为建议是因为通常你在运行时不需要“额外的”包装器,但其他时候它是必不可少的。忠告就是:忠告。

  • There is nothing that inherently prevents a `data` with a single strict field to be compiled with no indirection, is there? A language where what we know today as a newtype (with its `Coercible` instance) were just a special case of `data` seems completely plausible with the added benefit of having one fewer keyword, so the more interesting question, which I think is what OP is asking, is why that alternative design was not chosen?
  • I know these are different, but there is a semantic-preserving translation from "Haskell with newtypes" to "Haskell without newtypes" (modulo changing the target of `Coercible`). Whenever you write `(N x) -> f x` you could equivalently write ` ~(D x) -> f x`.

以上是为什么编译器不能在Haskell中为我们处理newtype?的全部内容。
THE END
分享
二维码
< <上一篇
下一篇>>