看起来 newtype
最初主要是程序员提供的注释,用于执行编译器无法自行解决的优化,有点像 C 语言中的 register
关键字。
然而,在 Haskell 中,newtype
不仅仅是编译器的建议注释;它实际上具有语义后果。这些类型:
newtype Foo = Foo Int
data Bar = Bar Int
声明两个非同构类型。具体来说,Foo undefined
和undefined :: Foo
是等效的,而Bar undefined
和undefined :: Bar
不是,因此:
Foo undefined `seq` "not okay"
Bar undefined `seq` "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"
case undefined of ~(Baz n) -> "okay"
换句话说,如果我奶奶有轮子,她就会成为一辆自行车!所以,为什么编译器不能在看到单值数据构造函数时自动应用这种优化呢?嗯,它不能在不改变程序语义的情况下普遍地执行此优化,因此需要首先证明在将特定的任意一个构造函数、一个字段的“data”类型强制为其字段,并对其进行无可辩驳的匹配而不是严格匹配时,语义未发生变化。由于这取决于实际使用类型的值的方式,因此对于由模块导出的数据类型(尤其是在函数调用边界处),这可能很难做到,但现有的优化机制(如专门化、内联、严格性分析和取消装箱)通常会在自包含代码块中执行等效优化,因此即使您意外使用了“data”类型,也可能获得“newtype”的好处。尽管如此,总的来说,似乎这对编译器来说是太困难的问题,因此记住要使用“newtype”的负担留给了程序员。
这带来了一个显而易见的问题——为什么我们不能更改语义,使它们等效;为什么首先“newtype”和“data”的语义不同呢?嗯,“newtype”语义的原因似乎很明显。由于“newtype”优化(在编译时擦除类型和构造函数)的性质,它变得不可能——或至少极其困难——在编译时分别表示“Foo undefined”和“undefined :: Foo”,这解释了这两个值的等价性。因此,在只有一个可能的构造函数并且没有可能出现该构造函数不存在的情况下(或者至少不能区分构造函数的存在和不存在的情况,因为唯一可能发生这种情况的情况是区分“Foo undefined”和“undefined :: Foo”,而我们已经说过编译代码中无法区分它们),无可辩驳的匹配是一个显然的进一步优化。
对于一个构造函数、一个字段的“data”类型的语义(在没有严格性注释和无可辩驳匹配的情况下),原因可能不太明显。但是,这些语义与构造函数和/或字段计数其他于1的数据类型是完全一致的,而“newtype”语义则会在一种特殊情况下引入任意的不一致性,即一个“data”类型和所有其他类型之间。由于“data”和“newtype”类型之间的这种历史差异,许多后续扩展都对它们进行了不同的处理,进一步巩固了不同的语义。您提到的“GeneralizedNewTypeDeriving”适用于“newtype”但不适用于一个构造函数、一个字段的“data”类型。在用于安全强制转换(即“Data.Coerce”)和“DerivingVia”计算表示上也有进一步的差异,存在量化或更一般的GADT的使用,“UNPACK”命令等。通用的表示类型在泛型中的表示方式也存在一些差异,虽然现在我认真看它们时,它们似乎相当表面。
即使“newtype”是一个不必要的历史错误,本质上可以通过特殊处理某个“data”类型来替代它,现在也有点晚了。此外,“newtype”并没有让
newtype
具有语义价值。它告诉我们开发者的意图:“我想要包装这种类型,也许隐藏一些功能,并以某种其他方式增强它。” - Gregory Higleynewtype
稍微减少了惰性,这有时是一种期望的效果,因为它没有真正的数据构造函数。 - Willem Van Onsemdata Foo a = Foo a
,编译器就知道它可以将其转换为newtype Foo a = Foo a
。对于单值数据构造函数,它始终可以这样做,那么为什么不将其隐式化并摆脱newtype
呢? - Gregory Higley