为什么Haskell中存在“数据”和“新类型”?

时间:2010-04-15 22:03:11

标签: haskell types language-design type-systems

似乎newtype定义只是一个服从某些限制的data定义(例如,只有一个构造函数),并且由于这些限制,运行时系统可以处理newtype更有效率。并且未定义值的模式匹配处理略有不同。

但是假设Haskell只知道data定义,没有newtype s:编译器不能自己发现给定的数据定义是否遵守这些限制,并自动更有效地对待它?

我确信我错过了某些事情,必须有更深层次的理由。

4 个答案:

答案 0 :(得分:174)

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完全相同,但编译器承诺不会让您混淆它们。

答案 1 :(得分:59)

根据Learn You a Haskell

  

使用newtype关键字代替data关键字。现在为什么   那?好吧,一个,newtype更快。如果您使用data关键字   包装类型,包装和展开所有包装的开销   当你的程序运行时。但是如果你使用newtype,Haskell知道   您只是使用它将现有类型包装成新类型   (因此得名),因为你希望它在内部相同但是   有不同的类型。考虑到这一点,Haskell可以摆脱它   包装和解包,一旦它解析了哪种类型的值。

     

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

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并返回值"你好"不管是不是布尔   在CoolBool里面是对还是错:

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

不要将此功能应用于普通的CoolBool,而是让它变成一个曲线球并将其应用于未定义的!

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

糟糕!一个例外!现在为什么会发生这种异常?定义的类型   使用data关键字可以有多个值构造函数(甚至   虽然CoolBool只有一个)。所以为了看看是否给出了价值   我们的函数符合(CoolBool _)模式,Haskell必须   评估该值足以查看使用了哪个值构造函数   当我们创造价值时。当我们试图评估一个未定义的   价值,甚至一点点,抛出异常。

     

不要使用CoolBool的data关键字,而是尝试使用   NEWTYPE:

newtype CoolBool = CoolBool { getCoolBool :: Bool }   
  

我们不必   更改我们的helloMe函数,因为模式匹配语法是   如果使用newtype或数据来定义类型,则相同。我们来做吧   同样的事情,并将helloMe应用于未定义的值:

ghci> helloMe undefined  
"hello"
  

有效!嗯,为什么?好吧,就像我们说的那样,当我们使用时   newtype,Haskell可以在内部表示新类型的值   与原始值的方式相同。它不必添加另一个   在它们周围的盒子里,它只需要知道它们的价值   不同种类。而且因为Haskell知道用这个类型制作的类型   newtype关键字只能有一个构造函数,它不需要   评估传递给函数的值以确保它   符合(CoolBool _)模式,因为newtype类型只能   有一个可能的值构造函数和一个字段!

     

这种行为上的差异可能看起来微不足道,但它实际上很漂亮   很重要,因为它有助于我们意识到即使定义了类型   数据和newtype的行为类似于程序员的观点   因为它们都有值构造函数和字段,所以它们是   实际上有两种不同而数据可用于制作   你自己的类型从头开始,newtype用于制作一个全新的   输入现有类型。新类型值的模式匹配不是   比如从盒子里取出一些东西(比如数据),还有更多   关于从一种类型直接转换到另一种类型。

这是另一个来源。根据{{​​3}}:

  

newtype声明以与数据大致相同的方式创建新类型。   newtypes的语法和用法几乎与   数据声明 - 实际上,您可以用new替换newtype关键字   数据和它仍然可以编译,确实有一个很好的机会你的   程序仍然有效。然而,相反的情况并非如此 - 数据可以   如果类型只有一个构造函数,则只能用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会毫无理由地改组字节。

请参阅this Newtype article了解杂乱的内容......

答案 2 :(得分:43)

痴迷于子弹列表的人的简单版本(找不到,所以必须自己写):

数据 - 使用值构造函数

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

newtype - 使用值构造函数

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

类型 - 为类型创建替代名称(同义词)(如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"

答案 3 :(得分:9)

脱离我的头顶;数据声明在访问和存储其“成员”时使用延迟评估,而newtype则不然。 Newtype还会从其组件中删除所有先前的类型实例,从而有效地隐藏其实现;而数据使实施开放。

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

在第一次阅读此内容时,我发现Haskell的温和介绍this chapter非常直观。