12

它写道,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)(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 中的“值构造函数”实际上是什么,与用于存储相同类型的位置值的元组是值构造函数之间是否存在实质性区别?论据?

4

2 回答 2

16

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

虽然有一些实际差异:

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

    data Tree a = EmptyTree | Node !a !(Tree a) !(Tree a)
    
  2. 内存占用和性能。绕过中间类型可减少占用空间并提高性能。你可以在这个很好的答案中阅读更多关于它的信息。

    您还可以使用 pragma 标记严格的字段,UNPACK进一步减少占用空间。或者,您可以使用编译-funbox-strict-fields器选项。关于最后一个,我只是更喜欢在我的所有项目中默认启用它。例如,参见Hasql 的 Cabal 文件


考虑到上述情况,如果您要查找的是惰性类型,则以下代码段应编译为相同的内容:

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

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

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

如果您追求的是严格的类型和占用空间减少,那么除了将元组直接非规范化为构造函数字段之外别无他法。

于 2014-12-15T02:33:19.787 回答
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))。您可以非常简单地编写在两者之间转换的函数。

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

于 2014-12-15T02:35:00.317 回答