【问题标题】:Non-tree data structures in HaskellHaskell 中的非树数据结构
【发布时间】:2014-01-18 17:01:23
【问题描述】:

在 Haskell 中制作树状数据结构相对容易。但是,如果我想要如下结构怎么办:

    A (root)
   / \
  B   C
 / \ / \
D   E   F

所以如果我通过 B 遍历结构来更新 E,如果我遍历 C,返回的新更新结构也更新了 E。

有人能给我一些关于如何实现这一目标的提示吗?你可以假设没有循环。

【问题讨论】:

  • 应该平等决定身份吗?更详细地说,假设DEF 都是Ints。如果DF 的值都恰好是5,那么当我更新一个时,另一个应该更新吗?
  • @icktoofay:为什么DF 应该以任何方式关联?他的示例图根本没有说明这一点。
  • @icktoofay:如果你更新 A->B->E,那么 A->C->E 应该更新。这是唯一的相关性。
  • @Zeta:Haskell 没有内置的身份概念,因此有必要建立一个身份,无论是通过相等还是明确使用STRefs 或类似的东西。它在某种程度上旨在让读者思考他们将如何确定两个对象在像 Haskell 这样具有引用透明性的语言中是否相同。
  • 您可能会发现this question 的答案很有帮助。

标签: haskell


【解决方案1】:

我会将数据结构展平为一个数组,然后对其进行操作:

import Data.Array

type Tree = Array Int -- Bounds should start at (1) and go to sum [1..n]
data TreeTraverse = TLeft TreeTraverse | TRight TreeTraverse | TStop

给定一些遍历方向(左、右、停止),很容易看出,如果我们向左走,我们只需将当前级别添加到我们的位置,如果我们向右走,我们还将当前位置加一:

getPosition :: TreeTraverse -> Int
getPosition = getPosition' 1 1
  where 
    getPosition' level pos (TLeft ts)  = getPosition' (level+1) (pos+level) ts
    getPosition' level pos (TRight ts) = getPosition' (level+1) (pos+level + 1) ts
    getPosition' _     pos (TStop)     = pos

在您的情况下,您想遍历 ABE 或 ACE:

traverseABE = TLeft $ TRight TStop
traverseACE = TRight $ TLeft TStop

由于我们现在已经如何获取元素的位置,并且Data.Array 提供了一些设置/获取特定元素的函数,我们可以使用以下函数来获取/设置树值:

getElem :: TreeTraverse -> Tree a -> a
getElem tt t = t ! getPosition tt

setElem :: TreeTraverse -> Tree a -> a -> Tree a
setElem tt t x = t // [(getPosition tt, x)]

要完成代码,让我们使用您的示例:

example = "ABCDEF"

exampleTree :: Tree Char
exampleTree = listArray (1, length example) example

并将所有内容发送至action

main :: IO ()
main = do
  putStrLn $ "Traversing from A -> B -> E: " ++ [getElem traverseABE exampleTree]
  putStrLn $ "Traversing from A -> C -> E: " ++ [getElem traverseACE exampleTree]
  putStrLn $ "exampleTree: " ++ show exampleTree ++ "\n"

  putStrLn $ "Setting element from A -> B -> E to 'X', "
  let newTree = setElem traverseABE exampleTree 'X'
  putStrLn $ "but show via A -> C -> E: " ++ [getElem traverseACE newTree]
  putStrLn $ "newTree: " ++ show newTree ++ "\n"

请注意,这很可能不是最好的方法,而是我想到的第一件事。

【讨论】:

  • 我是否遗漏了什么,或者这仅适用于特定示例?比如说,我希望 B(又名 D)的左孩子与 C(又名 F)的右孩子相同,而是有两个不同的 E。在我看来,它只是巧合地将两个 E 联系起来。
  • @icktoofay:这取决于人们如何阅读示例。不清楚他是想要一个通用的“许多节点可以指向另一个节点”(您显然需要标识),还是“在任何级别 q,节点 n 应该指向 n + q 和 n + q + 1”(您不需要身份)。我假设你已经回答了关于第一个陈述的问题,而我已经回答了后一个陈述。
  • @zeta:是的,你是对的,这应该是通用的。您认为我可以使用 ST monad 但将其隐藏在 runST 中以使其具有纯界面吗?不需要一个完整的例子(虽然它会很好)只是想知道我是否在找错树。
【解决方案2】:

一旦你建立了身份,就可以完成。

但首先你必须建立身份。

在许多语言中,值可以彼此不同但相等。以 Python 为例:

>>> a = [1]
>>> b = [1]
>>> a == b
True
>>> a is b
False

你想更新树的一个分支中的 E,并更新该元素 E 的所有其他元素。但是 Haskell 是引用透明的:它没有事物相同的概念目的;只有相等,甚至这并不适用于每个对象。

你可以做到这一点的一种方法是平等。说这是你的树:

    __A__
   /     \
  B       C
 / \     / \
1   2   2   3

然后我们可以遍历树并将所有 2 更新为 4。但在某些情况下,这并不是您想要的。

在 Haskell 中,如果你想在多个地方更新一个东西,你必须明确不是相同的东西。解决此问题的另一种方法是使用唯一整数标记每个不同的值,并使用该整数来确定身份:

              ____________A___________
             /                        \
            B                          C
           / \                        / \
(id=1)"foo"   (id=2)"bar"  (id=2)"bar"   (id=3)"baz"

然后我们可以更新标识为 2 的所有值。意外碰撞不会成为问题,因为除了故意的碰撞之外不会有任何碰撞。

这基本上是STRefIORef 所做的,除了它们将实际值提升到monad 的状态并隐藏身份。使用这些的唯一缺点是你需要让你的大部分代码都是单子的,但无论你做什么,你都可能不会轻易摆脱这一点。 (修改值而不是替换它们本身就是一种有效的做法。)

您提供的结构没有详细说明,因此无法根据您的用例定制示例,但这里有一个使用the ST monadTree 的简单示例:

import Control.Monad
import Control.Monad.ST
import Data.Tree
import Data.Traversable (traverse)
import Data.STRef

createInitialTree :: ST s (Tree (STRef s String))
createInitialTree = do
  [a, b, c, d, e, f] <- mapM newSTRef ["A", "B", "C", "D", "E", "F"]
  return $ Node a [ Node b [Node d [], Node e []]
                  , Node c [Node e [], Node f []]
                  ]

dereferenceTree :: Tree (STRef s a) -> ST s (Tree a)
dereferenceTree = traverse readSTRef

test :: ST s (Tree String, Tree String)
test = do
  tree <- createInitialTree
  before <- dereferenceTree tree
  let leftE = subForest (subForest tree !! 0) !! 1
  writeSTRef (rootLabel leftE) "new"  -- look ma, single update!
  after <- dereferenceTree tree
  return (before, after)

main = do
  let (before, after) = runST test
  putStrLn $ drawTree before
  putStrLn $ drawTree after

请注意,虽然我们只显式修改了左侧 E 值的值,但它在右侧也根据需要发生了变化。


我应该注意,这些不是唯一的方法。对于同一问题,可能还有许多其他解决方案,但它们都要求您明智地定义身份。只有完成后才能开始下一步。

【讨论】:

  • 我对我在这里混合TreeSTRef 的方式并不完全满意。你可能想要一个专门的Tree a,它有STRef (Tree a)s 作为孩子,所以你可以替换整个子树。在这里,我刚刚制作了标签STRefs,因为这对containers 更有效,但您可能不想在现实生活中这样做。 (另一方面,到处添加STRefs 会使遍历变得非常乏味。)
猜你喜欢
  • 1970-01-01
  • 2020-06-20
  • 2019-11-09
  • 1970-01-01
  • 2010-10-30
  • 2014-09-29
  • 1970-01-01
  • 2010-10-30
  • 1970-01-01
相关资源
最近更新 更多