为什么哈斯克尔有“ data”和“ newtype”?

看起来 newtype定义只是一个遵守某些限制(例如,只有一个构造函数)的 data定义,由于这些限制,运行时系统可以更有效地处理 newtype。对于未定义值的模式匹配处理略有不同。

但是假设 Haskell 只知道 data定义,而不知道 newtype: 编译器难道不能自己查明给定的数据定义是否遵守这些限制,并自动更有效地处理它吗?

我肯定错过了什么,一定有更深层次的原因。

38915 次浏览

数据声明在访问和存储它们的“成员”时使用惰性计算,而 newtype 不使用惰性计算。Newtype 还从它的组件中删除了所有以前的类型实例,有效地隐藏了它的实现; 而数据使实现保持开放。

在避免使用复杂数据类型中的样板代码时,我倾向于使用 newtype,因为在使用这些类型时,我不一定需要访问内部机制。这既加快了编译和执行的速度,又降低了使用新类型时的代码复杂性。

当我第一次读到这篇文章时,我发现《 Haskell 简介》的 这一章非常直观。

newtype和单构造函数 data都引入了单值构造函数,但 newtype引入的值构造函数是严格的,而 data引入的值构造函数是惰性的。所以如果你有

data D = D Int
newtype N = N Int

然后,N undefined等效于 undefined,并在计算时导致错误。但是 D undefined没有等价于 undefined,并且它可以被评估,只要你不尝试偷看内部。

编译器不能自己处理吗。

不,实际上不是这样,作为程序员,您可以决定构造函数是严格的还是懒惰的。要理解何时以及如何使构造函数严格或惰性,您必须比我更好地理解惰性计算。我坚持报告中的观点,即 newtype可供你重新命名一种现有类型,就像有几种不相容的度量方法:

newtype Feet = Feet Double
newtype Cm   = Cm   Double

两者在运行时的行为都与 Double完全相同,但编译器保证不会让您混淆它们。

根据 向你学习 Haskell:

使用 newtype 关键字而不是 data 关键字 那么,首先,newtype 更快。如果你使用 data 关键字 包装一个字体,包装和拆封都会有一些开销 但是如果你使用 newtype,Haskell 就会知道 您只是使用它将一个现有类型包装成一个新类型 (因此得名) ,因为您希望它在内部是相同的,但是 有一个不同的类型。有了这个想法,Haskell 可以摆脱 一旦它解析了哪个值是什么类型,就可以进行包装和展开。

那么,为什么不一直使用 newtype 而不是 data 呢, 当您使用 newtype 从现有类型创建新类型时 关键字时,只能有一个值构造函数和该值 构造函数只能有一个字段 具有多个值构造函数的类型,每个构造函数可以 有零个或多个字段:

data Profession = Fighter | Archer | Accountant


data Race = Human | Elf | Orc | Goblin


data PlayerCharacter = PlayerCharacter Race Profession

使用 newtype 时,只能使用一个构造函数和一个构造函数 场地。

现在考虑以下类型:

data CoolBool = CoolBool { getCoolBool :: Bool }

这是你的普通代数类型 它有一个值构造函数,它有一个字段 类型为 Bool 的函数。让我们创建一个模式匹配于 并返回值“ hello”,而不管 返回文章页面《酷池里的人》是真是假译者:

helloMe :: CoolBool -> String
helloMe (CoolBool _) = "hello"

与其将此函数应用于普通的 CoolBool,不如抛出一个曲线球并将其应用于未定义的!

ghci> helloMe undefined
"*** Exception: Prelude.undefined

哎呀! 一个异常! 为什么会发生这个异常? 类型已定义 使用 data 关键字可以有多个值构造函数(甚至 尽管 CoolBool 只有一个)。因此,为了看看给定的值是否 如果函数符合(CoolBool _)模式,Haskell 必须 计算值,只要足够看到使用了哪个值构造函数即可 当我们计算一个未定义的 值,即使只有一点点,也会引发异常。

与使用 CoolBool 的 data 关键字不同,让我们尝试使用 新字型:

newtype CoolBool = CoolBool { getCoolBool :: Bool }

我们不需要 更改 helloMe 函数,因为模式匹配语法是 如果使用 newtype 或数据来定义类型,也是一样的 将 helloMe 应用到一个未定义的值:

ghci> helloMe undefined
"hello"

- 成功了! 为什么?-就像我们说的,当我们使用 Newtype,Haskell 可以在内部表示新类型的值 就像原始值一样,不需要再加一个 它只需要知道 因为 Haskell 知道用 关键字只能有一个构造函数,不一定要有 计算传递给函数的值,以确保 符合(CoolBool _)模式,因为新类型只能 有一个可能的值构造函数和一个字段!

这种行为上的差异可能看起来微不足道,但实际上却是美好的 因为它帮助我们认识到,即使类型定义 with data and newtype behave similarly from the programmer's point of 视图,因为它们都有值构造函数和字段,所以它们是 实际上是两种不同的机制。而数据可以用来 你自己的类型从头开始,新类型是为了使一个完全新的 新类型值的模式匹配不是 就像从盒子里拿出东西一样(就像数据一样) ,它更多 关于从一种类型到另一种类型的直接转换。

这是另一个来源。根据 这篇新型文章:

Newtype 声明创建新类型的方式与数据类似。 新类型的语法和用法实际上与 Data 声明-实际上,可以用 数据,它仍然会编译,甚至有一个很好的机会你的 程序仍然可以工作。然而,相反的情况是不正确的——数据可以 只有在类型只有一个构造函数的情况下才用 newtype 替换 里面只有一个场。

一些例子:

newtype Fd = Fd CInt
-- data Fd = Fd CInt would also be valid


-- newtypes can have deriving clauses just like normal types
newtype Identity a = Identity a
deriving (Eq, Ord, Read, Show)


-- record syntax is still allowed, but only for one field
newtype State s a = State { runState :: s -> (s, a) }


-- this is *not* allowed:
-- newtype Pair a b = Pair { pairFst :: a, pairSnd :: b }
-- but this is:
data Pair a b = Pair { pairFst :: a, pairSnd :: b }
-- and so is this:
newtype Pair' a b = Pair' (a, b)

听起来很有限! 那么为什么有人使用 newtype 呢?

对只有一个字段的构造函数的限制 意味着新类型和字段类型是直接的 通讯:

State :: (s -> (a, s)) -> State s a
runState :: State s a -> (s -> (a, s))

或者用数学术语来说它们是同构的,这意味着 类型在编译时被检查,在运行时这两个类型可以是 处理基本上相同,没有开销或间接 通常与数据构造函数关联 特定类型的不同类型类实例,或者想要 一个类型抽象,你可以把它包装在一个新类型中,它将被考虑 与类型检查器不同,但在运行时是相同的 使用各种深奥的技巧,如幻影或递归类型 担心 GHC 无缘无故地移动大量字节。

参见 那篇文章了解混乱的部分..。

对于那些痴迷于子弹清单的人来说,这是一个简单的版本(找不到,只能自己写了) :

Data -使用值构造函数创建新的代数类型

  • 可以有多个值构造函数
  • 值构造函数是懒惰的
  • 值可以有多个字段
  • 影响编译和运行时,有运行时开销
  • 创建的类型是一个独特的新类型
  • 可以有自己的类型类实例
  • 当与值构造模式匹配比较时,将至少评估为弱磁头正常形式(WHNF) *
  • 用于创建新的数据类型(例如: Address { zip: : String,street: : String })

Newtype -使用值构造函数创建新的“装饰”类型

  • 只能有一个值构造函数
  • 值构造函数是严格的
  • 值只能有一个字段
  • 只影响编译,没有运行时开销
  • 创建的类型是一个独特的新类型
  • 可以有自己的类型类实例
  • 当与值构造模式匹配对比时,根本不能进行评估 *
  • 用于创建基于现有类型的更高层次的概念,这些类型具有不同的支持操作集,或者不能与原始类型互换(例如: Meter,Cm,Feet is Double)

Type -为类型创建一个替代名称(同义词)(如 C 中的 typedef)

  • 没有值构造函数
  • 没有田地
  • 只影响编译,没有运行时开销
  • 不创建新类型(只为现有类型创建新名称)
  • 不能有自己的类型类实例
  • 当与数据构造函数模式匹配时,其行为与原始类型相同
  • 用于基于具有相同支持操作集的现有类型创建更高级别的概念(例如: String is [ Char ])

关于模式匹配懒惰:

data DataBox a = DataBox Int
newtype NewtypeBox a = NewtypeBox Int


dataMatcher :: DataBox -> String
dataMatcher (DataBox _) = "data"


newtypeMatcher :: NewtypeBox -> String
newtypeMatcher (NewtypeBox _) = "newtype"


ghci> dataMatcher undefined
"*** Exception: Prelude.undefined


ghci> newtypeMatcher undefined
“newtype"