为什么Haskell中有“数据”和“新types”?
看起来,新的types定义只是一个服从某些限制的data
定义(例如,只有一个构造函数),并且由于这些限制,运行时系统可以更有效地处理新types。 而对未定义值的模式匹配的处理稍有不同。
但是假设Haskell只知道data
定义,没有新types:编译器newtype
发现给定的数据定义是否服从这些限制,并自动更有效地对待它?
我确定我错过了一些东西,这一定有一些更深的原因。
newtype
和single-constructor data
引入了单个值构造函数,但newtype
引入的值构造函数是严格的, data
引入的值构造函数是惰性的。 所以,如果你有
data D = D Int newtype N = N Int
那么N undefined
相当于undefined
并在评估时导致错误。 但D undefined
不等于undefined
,只要您不尝试偷看内部就可以进行评估。
编译器不能自己处理这个。
不,并不是真的 – 在程序员决定构造函数是严格还是懒惰的时候,这是一种情况。 为了理解什么时候以及如何使构造函数严格或者懒惰,你必须比我更好地理解懒惰的评估。 我坚持报告中的想法,即新types可以让你重新命名现有的types,就像有几种不同types的度量:
newtype Feet = Feet Double newtype Cm = Cm Double
在运行时都performance得和Double
一样,但编译器承诺不会让你混淆它们。
据了解你哈斯克尔 :
使用newtype关键字而不是data关键字。 那为什么呢? 对一个人来说,newtype更快。 如果你使用data关键字来包装一个types,那么当你的程序运行时,所有的包装和解包都会有一些开销。 但是,如果你使用newtype,Haskell知道你只是用它来把一个现有的types包装成一个新的types(因此名字),因为你希望它在内部是相同的,但是具有不同的types。 考虑到这一点,Haskell一旦解决了哪种types的值,就可以摆脱包装和解包。
那为什么不直接用newtype而不是数据呢? 那么,使用newtype关键字从现有types创build新types时,只能有一个值构造函数,而该值构造函数只能有一个字段。 但是通过数据,可以创build具有多个值构造函数的数据types,每个构造函数可以包含零个或多个字段:
data Profession = Fighter | Archer | Accountant data Race = Human | Elf | Orc | Goblin data PlayerCharacter = PlayerCharacter Race Profession
使用newtype时,只能使用一个字段的构造函数。
现在考虑下面的types:
data CoolBool = CoolBool { getCoolBool :: Bool }
这是用data关键字定义的普通的代数数据types。 它有一个值构造函数,它有一个types为Bool的字段。 让我们在CoolBool上创build一个模式匹配的函数,并返回值“hello”,无论CoolBool内部的Bool是True还是False:
helloMe :: CoolBool -> String helloMe (CoolBool _) = "hello"
而不是将这个函数应用到一个正常的CoolBool,让我们把它扔到一个曲线球,并将其应用到undefined!
ghci> helloMe undefined "*** Exception: Prelude.undefined
哎呀! 一个例外! 现在为什么会发生这种exception? 用data关键字定义的types可以有多个值构造函数(即使CoolBool只有一个)。 所以为了看看赋给我们函数的值是否符合(CoolBool _)模式,Haskell必须评估这个值才足以看到我们创build值时使用了哪个值构造函数。 而当我们试图评估一个未定义的值时,即使有一点,也会抛出一个exception。
而不是使用CoolBool的数据关键字,让我们尝试使用newtype:
newtype CoolBool = CoolBool { getCoolBool :: Bool }
我们不必更改我们的helloMe函数,因为如果使用newtype或数据来定义types,模式匹配语法是相同的。 让我们在这里做同样的事情,并将helloMe应用于一个未定义的值:
ghci> helloMe undefined "hello"
有效! 嗯,这是为什么? 那么就像我们所说的那样,当我们使用newtype的时候,Haskell可以像原始值一样在内部表示新types的值。 它不需要在它们周围添加另一个框,它只需要知道不同types的值。 而且由于Haskell知道使用newtype关键字创build的types只能有一个构造函数,所以不需要评估传递给函数的值,以确保它符合(CoolBool _)模式,因为newtypetypes只能有一个可能的值构造函数和一个字段!
这种行为上的差异可能看起来微不足道,但实际上它非常重要,因为它帮助我们认识到即使使用数据和新types定义的types与程序员的angular度类似,因为它们都具有值构造器和字段,实际上它们是两种不同的机制。 鉴于数据可以用来从头开始创build自己的types,newtype是用于从现有types中创build一个全新的types。 在newtype值上进行模式匹配不像从盒子中拿出某些东西(就像数据一样),而是直接从一种types转换到另一种types。
这是另一个来源。 根据这个Newtype文章 :
一个新types声明以与数据非常相似的方式创build一个新types。 newtypes的语法和用法与数据声明的语法和用法实际上是一样的 – 事实上,你可以用数据replacenewtype关键字,它仍然会被编译,确实有一个很好的机会,你的程序仍然可以工作。 然而反过来说,数据只能用newtype来replace,如果这个types只有一个构造函数的话,那里面只有一个字段。
一些例子:
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 sa = State { runState :: s -> (s, a) } -- this is *not* allowed: -- newtype Pair ab = Pair { pairFst :: a, pairSnd :: b } -- but this is: data Pair ab = Pair { pairFst :: a, pairSnd :: b } -- and so is this: newtype Pair' ab = Pair' (a, b)
听起来相当有限! 那么为什么有人使用newtype?
简短版本对一个字段的构造函数的限制意味着新的types和字段的types是直接对应的:
State :: (s -> (a, s)) -> State sa runState :: State sa -> (s -> (a, s))
或者用math术语来说,它们是同构的。 这意味着,在编译时检查types之后,在运行时,两种types可以基本相同,没有通常与数据构造函数关联的开销或间接寻址。 所以,如果你想为一个特定的types声明不同types的类实例,或者想要创build一个types抽象的types,你可以把它包装成一个新types,它将被视为与types检查器不同,但在运行时是相同的。 然后,您可以使用幻影或recursiontypes的各种深层次的技巧,而无需担心GHC洗牌桶字节。
看杂乱的位子的文章 …
简单的版本痴迷子弹名单(没有find一个,所以必须自己写):
数据 – 使用值构造函数创build新的代数types
- 可以有几个值构造函数
- 价值构造函数是懒惰的
- 值可以有几个字段
- 影响编译和运行时,有运行时间的开销
- 创build的types是一个独特的新types
- 可以有自己的types实例
- 当与值构造函数进行模式匹配时,将至less评估为弱头标准forms(WHNF)*
- 用于创build新的数据types(例如:地址{zip :: String,street :: String})
newtype – 用值构造函数创build新的“装饰”types
- 只能有一个值构造函数
- 值构造函数是严格的
- 价值只能有一个领域
- 仅影响编译,无运行时间开销
- 创build的types是一个独特的新types
- 可以有自己的types实例
- 当与值构造函数进行模式匹配时,根本不评估CAN *
- 用于根据现有types创build更高级别的概念,具有不同的受支持操作集或者与原始types不可互换(例如:Meter,Cm,Feet是Double)
types – 为types创build替代名称(同义词)(如C中的typedef)
- 没有价值的构造函数
- 没有领域
- 仅影响编译,无运行时间开销
- 没有创build新的types(只有现有types的新名称)
- 不能有自己的types实例
- 与数据构造函数进行模式匹配时,其行为与原始types相同
- 用于根据现有types创build更高级别的概念,并使用相同的一组受支持的操作(例如: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"
在我头顶上; 数据声明在访问和存储他们的“成员”时使用懒惰的评估,而newtype不会。 Newtype也从组件中删除所有以前的types实例,有效地隐藏了它的实现。 而数据则保持开放。
我倾向于在避免使用复杂数据types中的样板代码时使用newtype,在使用它们时我不一定需要访问内部。 这加快了编译和执行速度,并降低了使用新types时的代码复杂度。
在第一次阅读这篇文章时,我发现Haskell的这一章很直观。