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

mar*_*ngw 146 haskell types type-systems language-design

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

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

我确定我错过了什么,必须有更深层次的理由.

Nor*_*sey 179

两者newtype和单构造函数都data引入了单个值构造函数,但引入的值构造函数newtype是strict,并且引入的值构造函数data是惰性的.所以,如果你有

data D = D Int
newtype N = N Int
Run Code Online (Sandbox Code Playgroud)

然后N undefined相当于undefined并在评估时导致错误.但是,D undefined不是等同于undefined,并且可以,只要你不要试图偷看里面评价.

编译器无法为自己处理此问题.

不,不是真的 - 这是程序员你可以决定构造函数是严格还是懒惰的情况.要了解何时以及如何使构造函数严格或懒惰,您必须比我更好地理解惰性求值.我坚持报告中的想法,即newtype您可以重命名现有类型,例如有几种不同的不兼容测量类型:

newtype Feet = Feet Double
newtype Cm   = Cm   Double
Run Code Online (Sandbox Code Playgroud)

两者的行为与Double运行时完全相同,但编译器承诺不会让您混淆它们.

  • @Savui,一个例子是`data T = TT`.这是一种奇怪的类型,我不记得我在哪里研究它(这是一个博客,命名不同).但是`newtype`版本只有`undefined`居住,`data`版本居住着无限多的值:`undefined`,`T undefined`,`T(T undefined)`等等. (9认同)
  • @Norman Ramsey - "但是newtype引入的值构造函数是严格的,数据引入的值构造函数是懒惰的." 不是反过来吗?https://gist.github.com/4045780 (5认同)
  • 从应用程序的角度来看,由于newtype是现有类型的新名称,因此它用于为同一基础类型实现类的多个实例.如果你试图定义说'Ord Feet`和`Ord Cm`的实例,编译器就不会抱怨.而你不能将两个定义称为"Ord Double". (5认同)
  • @Savui我认为关键是`data`构造函数在Haskell中自然是懒惰的.实现单参数单构造函数数据类型与单个构造函数的单个参数相同表示的优化将改变程序行为.改变行为的优化并不仅仅是优化; 他们改变了语言的含义.Mercury实际上有这种优化,因为它是一种严格的语言,所以这不会改变行为. (3认同)
  • @RobStewart我相信它,因为有NEWTYPE没有解构实际发生的 - 他说:`情况不确定我的其等效 - >"确定"`(与`情况下未定义d的我 - >"确定"`在的情况下,数据). (2认同)

Ros*_*one 61

根据了解你一个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 
Run Code Online (Sandbox Code Playgroud)

使用newtype时,您只能使用一个具有一个字段的构造函数.

现在考虑以下类型:

data CoolBool = CoolBool { getCoolBool :: Bool } 
Run Code Online (Sandbox Code Playgroud)

这是使用data关键字定义的普通代数数据类型.它有一个值构造函数,它有一个类型为Bool的字段.让我们创建一个模式匹配CoolBool的函数,并返回值"hello",无论CoolBool中的Bool是True还是False:

helloMe :: CoolBool -> String  
helloMe (CoolBool _) = "hello"  
Run Code Online (Sandbox Code Playgroud)

而不是将此函数应用于普通的CoolBool,让我们抛出一个曲线球并将其应用于undefined!

ghci> helloMe undefined  
"*** Exception: Prelude.undefined  
Run Code Online (Sandbox Code Playgroud)

哎呀!一个例外!现在为什么会发生这种异常?使用data关键字定义的类型可以有多个值构造函数(即使CoolBool只有一个).因此,为了查看赋予函数的值是否符合(CoolBool _)模式,Haskell必须评估该值,以便在我们创建值时查看使用了哪个值构造函数.当我们尝试评估未定义的值时,即使是一点点,也会抛出异常.

不要使用CoolBool的data关键字,让我们尝试使用newtype:

newtype CoolBool = CoolBool { getCoolBool :: Bool }   
Run Code Online (Sandbox Code Playgroud)

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

ghci> helloMe undefined  
"hello"
Run Code Online (Sandbox Code Playgroud)

有效!嗯,为什么?好吧,正如我们所说,当我们使用newtype时,Haskell可以在内部以与原始值相同的方式表示新类型的值.它不必在它们周围添加另一个框,它只需要知道不同类型的值.并且因为Haskell知道使用newtype关键字创建的类型只能有一个构造函数,所以它不必评估传递给函数的值以确保它符合(CoolBool _)模式,因为newtype类型只能有一个可能的值构造函数和一个字段!

这种行为上的差异可能看起来微不足道,但它实际上非常重要,因为它帮助我们意识到即使用数据和新类型定义的类型从程序员的角度来看也类似,因为它们都有值构造函数和字段,它们实际上是两种不同的机制.虽然数据可用于从头开始创建自己的类型,但newtype用于从现有类型中创建一个全新的类型.对newtype值进行模式匹配并不像从盒子中取出一样(就像数据一样),它更多的是从一种类型到另一种类型的直接转换.

这是另一个来源.根据这篇Newtype文章:

newtype声明以与数据大致相同的方式创建新类型.newtypes的语法和用法实际上与数据声明的语法和用法相同 - 实际上,你可以用数据替换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)
Run Code Online (Sandbox Code Playgroud)

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

短版本对一个具有一个字段的构造函数的限制意味着新类型和字段的类型直接对应:

State :: (s -> (a, s)) -> State s a
runState :: State s a -> (s -> (a, s))
Run Code Online (Sandbox Code Playgroud)

或者在数学术语中它们是同构的.这意味着在编译时检查类型之后,在运行时,两种类型可以基本上相同,没有通常与数据构造函数关联的开销或间接.因此,如果要为特定类型声明不同的类型类实例,或者想要使类型为抽象,则可以将其包装在newtype中,并且它将被视为与类型检查器不同,但在运行时相同.然后,您可以使用各种深度欺骗,如幻像或递归类型,而无需担心GHC改组字节数据无缘无故.

请参阅文章的杂乱位...


won*_*ice 49

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

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

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

newtype - 使用值构造函数创建新的"装饰"类型

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


Dan*_*Dan 9

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

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

当我第一次阅读这篇文章时,我发现Haskell的一个温和的介绍这一章非常直观.