树与否(Haskell类型理解)
在“Haskell的温和介绍”中,我们对树的类型进行了这样的声明:树与否(Haskell类型理解),haskell,types,binary-tree,graph-theory,Haskell,Types,Binary Tree,Graph Theory,在“Haskell的温和介绍”中,我们对树的类型进行了这样的声明: data Tree a = Leaf a | Branch (Tree a) (Tree a) deriving (Eq,Ord,Show,Read) 让我们制作一些这种类型的值: a1 = Leaf 1 a2 = Leaf 2 a3 = Leaf 3 a4 = a1 `Branch` a2 a5 = a2 `Branch` a3 a6 = a4 `Branch` a5 在ghci中: *Main> :t a
data Tree a = Leaf a | Branch (Tree a) (Tree a)
deriving (Eq,Ord,Show,Read)
让我们制作一些这种类型的值:
a1 = Leaf 1
a2 = Leaf 2
a3 = Leaf 3
a4 = a1 `Branch` a2
a5 = a2 `Branch` a3
a6 = a4 `Branch` a5
在ghci中:
*Main> :t a6
a6 :: Tree Integer
但是a6
根本不是树,请参见:
a6
/ \
a4 a5
/ \ / \
a1 a2 a3
这张图中有一个循环!
怎么了?树的类型定义正确吗?
或者,在理解这个例子的过程中,我没有发现一些错误…简单的回答是,从概念上来说,
a2
“是一个树a
,而不是对树a
的引用。从这个意义上说,a6
看起来确实像
a6
/ \
a4 a5
/ \ / \
a1 a2 a2 a3
也就是说,树中有两个a2
的“副本”
实际上,由于所有值都是不可变的,因此实现可以使用相同的内存来表示a4
的右子级和a5
的左子级,但这两个节点在树a
类型表示的抽象级别上保持不同
为了真正有一个循环,需要有一种能够从
a2
到达a4
和a5
的概念,并且这种类型不提供任何这样的子到父链接的表示,这使得我们无法判断a4
的左子节点和a5
的右子节点是同一个节点,还是两个看起来完全相同的不同节点。对于这种数据类型,根本不存在区别。我们应该区分表达式的值和它的内存表示形式
例如,这两个表达式具有相同的值:
事实上,在Haskell中,无法区分上述表达式的计算结果。它们在语义上是等价的
Haskell实现(例如GHC)可以自由地在内存中以任何不破坏语义的方式表示这些值。例如:
“Hello”
,然后使用一对指针(p1,p2)
李>
“Hello”
存储在内存中一次,然后使用一对指针(p,p)
e1、e2
中的任何一种。实际上,GHC将前者用于e2
,后者用于e1
,但这并不重要
在你们的树中,同样的问题也出现了。您的a6
的值是一个树。GHC可能将该树表示为非树DAG(即,如果转换为无向图,则有一个循环的DAG),但这并不重要,它只是一个实现细节。重要的方面是,这种表示尊重树的语义
人们可能会想,如果值是树,为什么DAG表示是合理的。这是因为在Haskell中,我们无法比较GHC使用的底层“引用”p
。如果我们有一个比较这些引用的函数comparePtr::a->a->Bool
,我们可以通过使用comparePtr(fst e)(snd e)
来区分e1
,e2
之间的e
。这将严重破坏执行的可靠性。不过,在哈斯凯尔,我们没有这种情况
(从技术上讲,有一个
不安全的
函数可以做到这一点,但是不安全的
函数永远不应该用在“正常”代码中。我们通常假装这些函数不存在。)它不是树或非树,因为一张纸上的图片暗示了这一点。为什么你认为你可以在图片中只使用一次a2?如果您更改程序,使所有节点都包含数字42,您会绘制42一次还是4次?内置数据类型和用户定义数据类型之间没有实质性差异,因此42与Leaf 42处于同等地位。为了证明这不是一棵树,您需要生成一个Haskell程序来检测这个“循环”。而你就是不能。Haskell的语义不允许这样的程序存在。非常感谢,伙计们!我现在明白我的误解是什么了!DAG中的A是“非循环的”——你不是说它是一个有循环的DAG,而是一个节点有两个父节点的DAG。它的内存表示可能不是一棵树,但它的语义值当然仍然是一棵树,正如您所说。无论如何,他们都没有一个循环。@amalloy说得对。我的意思是,当DAG转换成无向图时,它有一个循环。我使用了“循环”一词,因为OP在问题中是为了同一个概念,所以为了让OP更容易理解,我将添加更多细节。
e1 = ("Hello", "Hello")
e2 = let s = "Hello" in (s, s)