为什么Haskell中有"data"和"newtype"?

169

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

但假设Haskell只知道data定义,而不知道newtype:编译器是否能够自动找出给定数据定义是否遵循这些限制,并自动更高效地处理它?

我确定我缺少某些东西,一定有更深层次的原因。


更好的选择是:http://www.haskell.org/haskellwiki/Type - user319822
4个回答

201

使用 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完全相同,但编译器保证不会让你混淆它们。


5
@Norman Ramsey说:“但是newtype引入的值构造函数是严格的,而data引入的值构造函数是惰性的。”这不是颠倒了吗?https://gist.github.com/4045780 - Rob Stewart
3
我认为重点在于Haskell中的数据构造函数天然是惰性的。实现优化使得只有一个参数和一个构造函数的数据类型与单个构造函数的单个参数相同,将改变程序行为。改变行为的优化实际上不只是优化;它们是对语言含义的改变。Mercury实际上具有此优化,因为它是一种严格的语言,所以这不会改变行为。 - Ben
2
@RobStewart 我认为这是因为使用 newtype 时没有实际进行解构 - 这相当于说 case undefined of i -> "ok"(与数据的情况下 case undefined of D i -> "ok" 不同)。 - ScootyPuff
9
@Savui,一个例子是data T = T T。这是一种奇怪的类型,我不记得在哪里看到过它被研究过(那是一个不同名字的博客)。但是,newtype版本只能由undefined来填充,而data版本可以被无限多个值填充:undefinedT undefinedT (T undefined)等等。 - luqui
5
就应用而言,newtype 是现有类型的新名称,它用于为同一基础类型实现类的多个实例。现在,如果您尝试定义 Ord FeetOrd Cm 的实例,编译器将不会抱怨。但是,您不能有两个 Ord Double 的定义。 - Niket Kumar
显示剩余3条评论

71
根据Learn You a Haskell
使用newtype关键字代替data关键字。为什么呢?首先,newtype更快。如果使用data关键字来包装类型,则在程序运行时需要将其全部包装和解包,这会带来一些开销。但是如果使用newtype,Haskell知道你只是将现有类型包装到新类型中(因此是名称),因为你希望它在内部是相同的,但具有不同的类型。考虑到这一点,Haskell可以在解析哪个值属于哪种类型后消除包装和解包。
那么为什么不总是使用newtype而不是data呢?好吧,当你使用newtype关键字从现有类型创建新类型时,你只能有一个值构造函数,而该值构造函数只能有一个字段。但是使用data,您可以创建具有多个值构造函数的数据类型,每个构造函数都可以有零个或多个字段:
data Profession = Fighter | Archer | Accountant  

data Race = Human | Elf | Orc | Goblin  

data PlayerCharacter = PlayerCharacter Race Profession 

使用newtype时,您只能使用一个具有一个字段的构造函数。现在考虑以下类型:
data CoolBool = CoolBool { getCoolBool :: Bool } 

这是一个使用data关键字定义的普通代数数据类型,它只有一个值构造器,其中包含一个类型为Bool的字段。让我们编写一个函数,对CoolBool进行模式匹配,并返回值"hello",无论CoolBool内部的Bool是True还是False。请保留HTML标签。
helloMe :: CoolBool -> String  
helloMe (CoolBool _) = "hello"  

不要将此函数应用于普通的CoolBool,让我们给它一个曲线球,并将其应用于未定义的变量!

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

哎呀!出现了异常!为什么会有这个异常呢?使用data关键字定义的类型可以具有多个值构造函数(即使CoolBool只有一个)。所以,为了查看传递给函数的值是否符合(CoolBool _)模式,Haskell必须评估该值,仅仅用于查看创建该值时使用了哪个值构造函数。当我们尝试对未定义的值进行甚至是一点点的求值时,就会抛出异常。
让我们尝试使用newtype而不是data关键字来定义CoolBool:
newtype CoolBool = CoolBool { getCoolBool :: Bool }   

我们不需要改变helloMe函数,因为使用newtype或data定义类型时,模式匹配语法是相同的。让我们在这里做同样的事情,并将helloMe应用于未定义的值:
ghci> helloMe undefined  
"hello"

它奏效了!嗯,为什么呢?好吧,就像我们所说的,当我们使用newtype时,Haskell可以在内部以与原始值相同的方式表示新类型的值。它不必在它们周围添加另一个框,只需知道这些值是不同类型的即可。并且因为Haskell知道用newtype关键字制作的类型只能有一个构造函数,所以它不必评估传递给函数的值,以确保它符合(CoolBool _)模式,因为newtype类型只能有一个可能的值构造函数和一个字段!
这种行为上的差异可能看起来微不足道,但实际上非常重要,因为它帮助我们意识到,尽管使用data和newtype定义的类型从程序员的角度看行为类似,因为它们都有值构造函数和字段,但它们实际上是两种不同的机制。而data可以用于从头开始创建自己的类型,newtype则是用现有类型创建全新的类型。对newtype值进行模式匹配不像从盒子中取出东西那样(就像使用data一样),它更像是直接从一种类型转换为另一种类型。

这里有另一份资料。根据这篇Newtype文章所述:

newtype声明会创建一个新类型,方法和data相差无几。 newtype的语法和用法与data声明几乎相同-实际上,您可以将newtype关键字替换为data,编译仍然可以通过,事实上,您的程序甚至可能仍然能正常工作。然而,反之不成立-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)

Sounds pretty limited! So why does anyone use newtype?

The short version The restriction to one constructor with one field means that the new type and the type of the field are in direct correspondence:

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

or in mathematical terms they are isomorphic. This means that after the type is checked at compile time, at run time the two types can be treated essentially the same, without the overhead or indirection normally associated with a data constructor. So if you want to declare different type class instances for a particular type, or want to make a type abstract, you can wrap it in a newtype and it'll be considered distinct to the type-checker, but identical at runtime. You can then use all sorts of deep trickery like phantom or recursive types without worrying about GHC shuffling buckets of bytes for no reason.

请参阅 该文章 以了解混乱的细节...

1
事实上,您可以将 newtype 关键字替换为 data,并且它仍然可以编译。甚至有很大的机会你的程序仍然能够正常工作。不幸的是,在替换后以前可以工作的程序可能会失败 - 在底部和未定义方面存在一些差异。但我无法确定这些差异的具体内容。 - Matt Fenwick
多么精彩的解释! - Amit Erandole

58

对于着迷于项目符号列表的人的简化版本(找不到一个,所以只能自己写):

data - 创建具有值构造函数的新代数类型

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

newtype - 创建具有值构造函数的新“装饰”类型

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

type - 创建一个类型的替代名称(同义词)(类似于C中的typedef)

  • 没有值构造函数
  • 没有字段
  • 仅影响编译,没有运行时开销
  • 没有创建新类型(只是现有类型的新名称)
  • 不能有自己的类型类实例
  • 在模式匹配数据构造器时,与原始类型相同
  • 用于基于现有类型创建更高级别的概念,具有相同的支持操作集(例如:String 是 [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"

dataMatcher :: DataBox a -> String,newtypeMatcher :: NewtypeBox a -> String,否则我无法编译。 - 1234
我的代码没有收到任何异常? - 1234

9
从我的角度来看,数据声明在访问和存储其“成员”时使用惰性求值,而newtype则不是。 Newtype还从其组件中剥离了所有先前的类型实例,有效地隐藏了其实现;而数据则保留了实现。
我倾向于在避免繁琐代码的复杂数据类型中使用newtype,而我并不一定需要在使用它们时访问内部。这加快了编译和执行速度,并减少了新类型使用时的代码复杂性。
当我第一次阅读关于此的内容时,我发现Haskell温和介绍的这一章相当直观。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接