繁体   English   中英

我应该避免在Haskell中构建吗?

[英]Should I avoid constructing in Haskell?

在阅读Haskell for the Good的剪辑时,我发现了以下情况:

treeInsert :: (Ord a) => a -> Tree a -> Tree a  
treeInsert x EmptyTree = singleton x  
treeInsert x (Node a left right)   
  | x == a = Node x left right  
  | x < a  = Node a (treeInsert x left) right  
  | x > a  = Node a left (treeInsert x right)

如果我们只是在x == a时重用给定的树,那么性能会不会更好?

treeInsert :: (Ord a) => a -> Tree a -> Tree a  
treeInsert x EmptyTree = singleton x  
treeInsert x all@(Node a left right)   
  | x == a = all  
  | x < a  = Node a (treeInsert x left) right  
  | otherwise  = Node a left (treeInsert x right)

在现实生活中编码,我该怎么办? 返回同样的东西有什么缺点吗?

让我们来看看核心! (这里没有优化)

$ ghc-7.8.2 -ddump-simpl wtmpf-file13495.hs

相关的区别是第一个版本(没有all@(...)

                case GHC.Classes.> @ a_aUH $dOrd_aUV eta_B2 a1_aBQ
                of _ [Occ=Dead] {
                  GHC.Types.False ->
                    Control.Exception.Base.patError
                      @ (TreeInsert.Tree a_aUH)
                      "wtmpf-file13495.hs:(9,1)-(13,45)|function treeInsert"#;
                  GHC.Types.True ->
                    TreeInsert.Node
                      @ a_aUH
                      a1_aBQ
                      left_aBR
                      (TreeInsert.treeInsert @ a_aUH $dOrd_aUV eta_B2 right_aBS)

将节点重新用于as-pattern的地方就是这样

                TreeInsert.Node
                  @ a_aUI
                  a1_aBR
                  left_aBS
                  (TreeInsert.treeInsert @ a_aUI $dOrd_aUW eta_B2 right_aBT);

这是一种避免检查,可能会产生显着的性能差异。

但是 ,这种差异实际上与as-pattern无关。 这只是因为你的第一个代码片段使用的是x > a后卫,这并非易事。 第二种是otherwise使用,它被优化掉了。

如果您将第一个代码段更改为

treeInsert :: (Ord a) => a -> Tree a -> Tree a  
treeInsert x EmptyTree = singleton x  
treeInsert x (Node a left right)   
  | x == a     = Node x left right  
  | x < a      = Node a (treeInsert x left) right  
  | otherwise  = Node a left (treeInsert x right)

然后差异归结为

      GHC.Types.True -> TreeInsert.Node @ a_aUH a1_aBQ left_aBR right_aBS

VS

      GHC.Types.True -> wild_Xa

这确实只是Node x left right vs all的区别。

......没有优化,就是这样。 当我打开-O2时,版本会进一步分化 但我无法弄清楚性能如何不同。

在现实生活中编码,我该怎么办? 返回同样的东西有什么缺点吗?

a == b 保证fa == fb的所有功能f 因此,即使比较相等,您也可能必须返回新对象。

换句话说,它可能不可行的改变Node x left rightNode a left rightalla == x无论性能提升。

例如,您可能有携带元数据的类型。 当您将它们进行相等性比较时,您可能只关心值并忽略元数据。 但是如果你只是因为他们比较相等而替换它们那么你将会丢失元数据。

newtype ValMeta a b = ValMeta (a, b)  -- value, along with meta data
    deriving (Show)

instance Eq a => Eq (ValMeta a b) where
    -- equality only compares values, ignores meta data
    ValMeta (a, b) == ValMeta (a', b') = a == a'

要点是Eq类型类, 表示您可以比较相等的值。 除此之外,它不保证任何东西。

一个真实的例子,其中a == b不保证fa == fb是在自平衡树中维护一Set唯一值。 自平衡树(例如红黑树)对树的结构有一些保证,但实际深度和结构取决于数据被添加到集合中或从集合中移除的顺序。

现在,当您比较2组的相等性时,您希望比较集合中的值是否相等,而不是基础树具有相同的确切结构。 但是如果你有一个像depth这样的函数来暴露维护集合的底层树的深度,那么即使集合比较相等,你也无法保证深度相等。

这是伟大的菲利普·瓦德勒(Philip Wadler)在现场和舞台上实现的视频, 许多有用的关系不能保持平等 (从42分钟开始)。

编辑:来自ghc的示例,其中a == b并不意味着fa == fb

\> import Data.Set
\> let a = fromList [1, 2, 3, 4, 5, 10, 9, 8, 7, 6]
\> let b = fromList [1..10]
\> let f = showTree
\> a == b
True
\> f a == f b
False

另一个现实世界的例子是哈希表。 当且仅当它们的键值对结合时,两个哈希表是相等的。 但是,哈希表的容量 ,即在重新分配和重新分配之前可能添加的密钥数量,取决于插入/删除的顺序。

因此,如果您有一个返回哈希表容量的函数,它可能会为哈希表ab返回不同的值,即使a == b

我的两分钱......也许甚至不是原来的问题:

我不会用x < ax == a来编写警卫,而是将compare abLTEQGT compare ab匹配,例如:

treeInsert x all@(Node a left right) =
  case compare x a of
    EQ -> ...
    LT -> ...
    GT -> ...

如果xa可以是复杂的数据结构,我会这样做,因为像x < a这样的测试可能很昂贵。

回答似乎是错误的。 我把它留在这里,供参考......


使用第二个函数可以避免创建新节点,因为编译器无法真正理解相等( ==只是某个函数。)如果将第一个版本更改为

-- version C
treeInsert :: (Ord a) => a -> Tree a -> Tree a  
treeInsert x EmptyTree = singleton x  
treeInsert x (Node a left right)   
  | x == a = Node a left right  -- Difference here! Changed x to a.
  | x < a  = Node a (treeInsert x left) right  
  | x > a  = Node a left (treeInsert x right)

编译器可能会执行公共子表达式消除,因为优化器将能够看到Node a left right Node a left right相同。

另一方面,我怀疑编译器可以从a == x推断出Node a left right Node x left rightNode x left right相同。

所以,我很确定在-O2 ,版本B和版本C下是相同的,但是版本A可能更慢,因为它在a == x情况下进行了额外的实例化。

好吧,如果第一个案例使用a而不是x ,那么GHC至少有可能通过公共子表达式消除来消除新节点的分配。

treeInsert x (Node a left right)   
  | x == a = Node a left right

但是,这在任何非平凡的用例中都是无关紧要的,因为即使元素已经存在,树下到节点的路径也将被复制。 除非您的用例很简单,否则此路径将比单个节点长得多。

在ML的世界中,避免这种情况的相当惯用的方法是抛出KeyAlreadyExists异常,然后在顶级插入函数中捕获该异常并返回原始树。 这将导致堆栈展开,而不是在堆上分配任何Node

直接实现ML习语在Haskell中基本上是禁止的,原因很充分。 如果避免这种重复很重要,最简单也可能最好的做法是在插入之前检查树是否包含密钥。

与直接Haskell插入或ML习语相比,这种方法的缺点是它涉及两次遍历路径而不是一次。 现在,这是一个可以在Haskell中实现的非重复的单遍插入:

treeInsert :: Ord a => a -> Tree a -> Tree a
treeInsert x original_tree = result_tree
  where
    (result_tree, new_tree) = loop x original_tree

    loop x EmptyTree = (new_tree, singleton x)
    loop x (Node a left right) =
       case compare x a of
         LT -> let (res, new_left) = loop x left
                in (res, Node a new_left right)
         EQ -> (original_tree, error "unreachable")
         GT -> let (res, new_right) = loop x right
                in (res, Node a left new_right)

然而,旧版本的GHC(大约7到10年前)不能非常有效地通过惰性结果对处理这种递归,并且根据我的经验,在插入前检查可能会表现得更好。 如果这个观察结果在最近的GHC版本中发生了变化,我会感到有些惊讶。

可以想象一个函数直接构造(但不返回)树的新路径,并且一旦知道元素是否已经存在就决定返回新路径或原始路径。 (如果没有返回,新路径将立即变为垃圾。)这符合GHC运行时的基本原则,但在源语言中并不真实。

当然,对于惰性数据结构,任何完全不重复的插入函数都将具有与简单的重复插入不同的严格性。 所以无论实现技术如何,如果懒惰很重要,它们就是不同的功能。

但是,当然,路径是否重复可能并不重要。 最重要的情况是当你持续使用树时,因为在线性使用情况下,旧路径在每次插入后会立即变成垃圾。 当然,这只在您插入大量重复项时才有意义。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM