似乎newtype
定义只是一个服从某些限制的data
定义(例如,只有一个构造函数),并且由于这些限制,运行时系统可以处理newtype
更有效率。并且未定义值的模式匹配处理略有不同。
但是假设Haskell只知道data
定义,没有newtype
s:编译器不能自己发现给定的数据定义是否遵守这些限制,并自动更有效地对待它?
我确信我错过了某些事情,必须有更深层次的理由。
答案 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)
使用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)
痴迷于子弹列表的人的简单版本(找不到,所以必须自己写):
数据 - 使用值构造函数
创建新的代数类型newtype - 使用值构造函数
创建新的“装饰”类型类型 - 为类型创建替代名称(同义词)(如C中的typedef)
[*]关于模式匹配懒惰:
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非常直观。