值构造函数和元组之间有什么区别?

时间:2014-12-15 02:13:10

标签: haskell tuples algebraic-data-types value-constructor

It's written Haskell元组只是代数数据类型的不同语法。类似地,有一些示例说明如何使用元组重新定义值构造函数。

例如,Haskell中的Tree数据类型可能写为

data Tree a = EmptyTree | Node a (Tree a) (Tree a)

可以像这样转换为“元组形式”:

data Tree a = EmptyTree | Node (a, Tree a, Tree a)

第一个示例中的Node值构造函数与第二个示例中的实际tuple之间有什么区别?即Node a (Tree a) (Tree a) vs. (a, Tree a, Tree a)(除了语法之外)?

在幕后,Node a (Tree a) (Tree a)是不是每个位置的3元组相应类型的语法不同?

我知道您可以部分应用值构造函数,例如Node 5,其类型为:(Node 5) :: Num a => Tree a -> Tree a -> Tree a

您可以使用(,,)作为函数来部分应用元组...但是这不知道未绑定条目的潜在类型,例如:

Prelude> :t (,,) 5
(,,) 5 :: Num a => b -> c -> (a, b, c)

除非,我猜,你明确声明了一个::的类型。

除了像这样的语法专业,再加上类型作用域的最后一个例子,Haskell中实际存在的“值构造函数”与用于存储相同类型的位置值的元组之间是否存在实质差异值构造函数的参数?

2 个答案:

答案 0 :(得分:16)

嗯,在概念上确实没有区别,实际上其他语言(OCaml,Elm)正是以这种方式呈现标记的联合 - 即标记在元组或第一类记录(Haskell缺乏)上。我个人认为这是Haskell的设计缺陷。

但是有一些实际的差异:

  1. 懒惰。 Haskell的元组是懒惰的,你无法改变它。但是,您可以将构造函数字段标记为严格:

    data Tree a = EmptyTree | Node !a !(Tree a) !(Tree a)
    
  2. 内存占用和性能。规避中间类型可减少占地面积并提高性能。您可以在this fine answer中详细了解相关信息。

    您还可以使用the UNPACK pragma标记严格字段,以进一步减少占用空间。或者,您可以使用the -funbox-strict-fields compiler option。关于最后一个,我只是希望在我的所有项目中默认启用它。例如,请参阅the Hasql's Cabal file


  3. 考虑到上述情况,如果它是您正在寻找的懒惰类型,那么以下代码段应编译为相同的内容:

    data Tree a = EmptyTree | Node a (Tree a) (Tree a)
    
    data Tree a = EmptyTree | Node {-# UNPACK #-} !(a, Tree a, Tree a)
    

    所以我想你可以说可以使用元组来存储构造函数的惰性字段而不会受到惩罚。虽然应该提到这种模式在Haskell的社区中是非常规的。

    如果这是你所追求的严格类型和足迹减少,那么除了将元组直接反化为构造函数字段之外别无他法。

答案 1 :(得分:11)

他们称之为 isomorphic ,意思是"具有相同的形状"。你可以写点像

data Option a = None | Some a

这与

同构
data Maybe a = Nothing | Just a

意味着您可以编写两个函数

f :: Maybe a -> Option a
g :: Option a -> Maybe a

对于所有可能的输入,f . g == id == g . f。然后我们可以说(,,)是一个与构造函数同构的数据构造函数

data Triple a b c = Triple a b c

因为你可以写

f :: (a, b, c) -> Triple a b c
f (a, b, c) = Triple a b c

g :: Triple a b c -> (a, b, c)
g (Triple a b c) = (a, b, c)

Node作为构造函数是Triple的特例,即Triple a (Tree a) (Tree a)。事实上,你甚至可以说你的Tree的定义可以写成

newtype Tree' a = Tree' (Maybe (a, Tree' a, Tree' a))

newtype是必需的,因为您不能递归type别名。您所要做的就是说EmptyLeaf == Tree' NothingNode a l r = Tree' (Just (a, l, r))。你可以简单地编写在两者之间转换的函数。

请注意,这完全来自数学观点。编译器可以添加额外的元数据和其他信息,以便能够识别特定的构造函数,使它们在运行时的行为略有不同。