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

ely*_*ely 12 haskell tuples algebraic-data-types value-constructor

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

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

data Tree a = EmptyTree | Node a (Tree a) (Tree a)
Run Code Online (Sandbox Code Playgroud)

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

data Tree a = EmptyTree | Node (a, Tree a, Tree a)
Run Code Online (Sandbox Code Playgroud)

Node第一个示例中的值构造函数tuple与第二个示例中的值构造函数有什么区别?ie 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)
Run Code Online (Sandbox Code Playgroud)

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

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

Nik*_*kov 16

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

但是有一些实际的差异:

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

    data Tree a = EmptyTree | Node !a !(Tree a) !(Tree a)
    
    Run Code Online (Sandbox Code Playgroud)
  2. 内存占用和性能.规避中间类型可减少占地面积并提高性能.您可以在这个精美的答案中阅读更多相关信息.

    您还可以标记与严格的领域的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)
Run Code Online (Sandbox Code Playgroud)

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

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


bhe*_*ilr 11

它们就是所谓的同构,意思是"具有相同的形状".你可以写类似的东西

data Option a = None | Some a
Run Code Online (Sandbox Code Playgroud)

这是同构的

data Maybe a = Nothing | Just a
Run Code Online (Sandbox Code Playgroud)

意思是你可以写两个函数

f :: Maybe a -> Option a
g :: Option a -> Maybe a
Run Code Online (Sandbox Code Playgroud)

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

data Triple a b c = Triple a b c
Run Code Online (Sandbox Code Playgroud)

因为你可以写

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)
Run Code Online (Sandbox Code Playgroud)

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

newtype Tree' a = Tree' (Maybe (a, Tree' a, Tree' a))
Run Code Online (Sandbox Code Playgroud)

这newtype是必需的,因为你不能有一个type别名是递归的.你所要做的就是说EmptyLeaf == Tree' Nothing和Node a l r = Tree' (Just (a, l, r)).您可以非常简单地编写在两者之间进行转换的函数.

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