为啥 Haskell 中有“数据”和“新类型”? [复制]

Posted

技术标签:

【中文标题】为啥 Haskell 中有“数据”和“新类型”? [复制]【英文标题】:Why is there "data" and "newtype" in Haskell? [duplicate]为什么 Haskell 中有“数据”和“新类型”? [复制] 【发布时间】:2011-02-08 14:37:43 【问题描述】:

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

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

我确定我错过了什么,这一定有更深层次的原因。

【问题讨论】:

更好:haskell.org/haskellwiki/Type 【参考方案1】:

为痴迷子弹列表的人准备的简单版本(找不到,只好自己写了):

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

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

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

只能有一个值构造函数 值构造函数是严格的 值只能有一个字段 仅影响编译,无运行时开销 创建的类型是一种独特的新类型 可以有自己的类型类实例 当与值构造函数进行模式匹配时,根本无法评估 * 用于基于现有类型创建更高级别的概念,该类型具有不同的支持操作集或与原始类型不可互换(例如:米、厘米、英尺为双精度)

type - 为类型创建替代名称(同义词)(如 C 中的 typedef)

无值构造函数 没有字段 仅影响编译,无运行时开销 未创建新类型(仅为现有类型创建新名称) 不能有自己的类型类实例 对数据构造函数进行模式匹配时,行为与原始类型相同 用于基于现有类型创建更高级别的概念,支持相同的操作集(例如:字符串为 [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,否则无法编译 justpaste.it/371g3 我的代码没有得到任何异常?..【参考方案2】:

根据Learn You a Haskell:

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

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

data Profession = Fighter | Archer | Accountant  

data Race = Human | Elf | Orc | Goblin  

data PlayerCharacter = PlayerCharacter Race Profession 

使用 newtype 时,您只能使用一个构造函数和一个 字段。

现在考虑以下类型:

data CoolBool = CoolBool  getCoolBool :: Bool  

这是您定义的普通代数数据类型 数据关键字。它有一个值构造函数,它有一个字段 其类型为 Bool。让我们创建一个模式匹配的函数 CoolBool 并返回值“hello”,无论 Bool 是否 CoolBool 里面是 True 还是 False:

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

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

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

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

我们不使用CoolBool 的data 关键字,而是尝试使用 新类型:

newtype CoolBool = CoolBool  getCoolBool :: Bool    

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

ghci> helloMe undefined  
"hello"

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

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

这是另一个来源。根据this Newtype article:

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)

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

简短版 一个构造函数和一个字段的限制 表示新类型和字段的类型是直接的 对应:

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

或者在数学术语中它们是同构的。这意味着之后 在编译时检查类型,在运行时这两种类型可以 处理基本相同,没有开销或间接 通常与数据构造函数相关联。所以如果你想声明 特定类型的不同类型类实例,或想要制作 一个类型抽象,你可以把它包装在一个新类型中,它会被考虑 与类型检查器不同,但在运行时相同。然后你可以 使用各种深层技巧,例如幻像或递归类型,而无需 担心 GHC 无缘无故地洗牌桶。

请参阅the article 了解混乱的部分...

【讨论】:

in fact, you can replace the newtype keyword with data and it'll still compile, indeed there's even a good chance your program will still work 不幸的是,替换后以前工作的程序可能会失败——在底部和未定义方面存在一些差异。希望我能准确地说出这些差异,但不确定。 多么神奇的解释!【参考方案3】:

在我的头顶上;数据声明在访问和存储其“成员”时使用惰性求值,而 newtype 则没有。 Newtype 还从它的组件中剥离了所有以前的类型实例,有效地隐藏了它的实现;而数据使实现保持打开状态。

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

当我第一次阅读这篇文章时,我发现 this chapter 的 Haskell 简介相当直观。

【讨论】:

【参考方案4】:

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

【讨论】:

@Norman Ramsey - “但是 newtype 引入的值构造函数是严格的,而 data 引入的值构造函数是惰性的。”不是反过来吗? gist.github.com/4045780 @Savui 我认为重点是 data 构造函数在 Haskell 中自然是惰性的。实现单参数单构造函数数据类型与单构造函数的单参数表示相同的优化将改变程序行为。改变行为的优化不仅仅是优化;它们是语言含义的变化。 Mercury 实际上有这种优化,因为它是一种严格的语言,所以不会改变行为。 @RobStewart 我相信这是因为新类型实际上并没有发生解构——它相当于说case undefined of i -> "ok"(在数据的情况下与case undefined of D i -> "ok" 相比)。 @Savui,一个例子是data T = T T。这是一种奇怪的类型,我不记得我在哪里看到它研究过(它是一个博客,名称不同)。但是newtype 版本只包含undefineddata 版本包含无限多个值:undefinedT undefinedT (T undefined) 等。 从应用程序的角度来看,由于newtype是现有类型的新名称,它用于为同一底层类型实现类的多个实例。现在,如果您尝试定义 Ord FeetOrd Cm 的实例,编译器不会抱怨。而你不能有两个定义为Ord Double

以上是关于为啥 Haskell 中有“数据”和“新类型”? [复制]的主要内容,如果未能解决你的问题,请参考以下文章

为啥我的 Haskell 函数参数必须是 Bool 类型?

为啥 Haskell 没有在函数签名中推断数据类型的类型类?

Haskell Lesson:类型系统解读

Haskell - 新类型上的 iso

如何使用haskell类型系统来描述关系,从而防止出现更多错误

为啥 Haskell 没有符号(a la ruby​​)/原子(a la erlang)?