我遇到了以下问题:我有一个不同类的对象树,其中子类中的操作使父级无效。在命令式语言中,这样做很简单。例如,在Java中:
public class A {
private List<B> m_children = new LinkedList<B>();
private boolean m_valid = true;
public void invalidate() {
m_valid = false;
}
public void addChild(B child) {
m_children.add(child);
child.m_parent = this;
}
}
public class B {
public A m_parent = null;
private int m_data = 0;
public void setData(int data) {
m_data = 0;
m_parent.invalidate();
}
}
public class Main {
public static void main(String[] args) {
A a = new A();
B b = new B();
b.setData(0); //invalidates A
}
}
如何在Haskell中执行上述操作?我无法解决这个问题,因为一旦我在Haskell中构造一个对象,它就无法改变。
如果发布相关的Haskell代码,我将非常感激。
编辑:我想解决的问题如下:
我有一个编辑文档的应用程序。文档是对象的层次结构。修改子对象的属性时,需要将文档设置为无效状态,以便用户知道需要验证文档。
答案 0 :(得分:7)
在Huet原始论文的术语中,修改一个可能需要频繁偏移到根和后面的路径的树似乎是具有“疤痕”的Zipper数据结构的变体的完美工作;来自论文的代码样本也提出了“记忆拉链”的名称。当然,经过一些小心操作,也可以使用常规拉链,但增强版可能更方便和/或更高效。
基本思想与普通拉链背后的理念相同,它已经允许人们以纯粹的功能方式上下移动树(没有任何明确的反向指针),但是“上行”操作后跟“下行”操作变为无操作,将焦点留在原始节点上(而使用常规拉链则将其移动到原始节点的最左侧兄弟)。
以下是该论文的链接:Gérard Huet, Functional Pearl: The Zipper。这只是六页,但其中包含的想法对任何功能程序员都非常有用。
答案 1 :(得分:3)
要回答标题中的问题:是的,您可以创建与父母及其子女有链接的节点。例如:
-- parent children
data Tree = Node (Maybe Tree) [Tree]
root = Node Nothing [a,b] -- I can "forward reference" a and b because haskell is lazy
a = Node (Just root) []
b = Node (Just root) []
问题是这是否对您的特定用例有用(通常不是这样)。
现在你身体里的问题:你是对的,你不能在创建后改变它。所以一旦你有了一个有效的树,只要引用那棵树的变量在范围内,你就会有一个有效的树。
你没有真正描述你想要解决的问题,所以我不能告诉你如何在功能上模拟你想要做的事情,但我确信有一种方法可以不改变树。
答案 2 :(得分:3)
这是一些拉链代码,它演示了光标指向的数据的简单修改以及树的“全局”属性。我们构建一个树,将光标移动到最初包含1的节点,将其更改为3,并在完全更新的树中留下指向该节点的光标。
import Data.Maybe (fromJust)
import Data.Tree
import Data.Tree.Zipper
type NodeData = Either Bool Int
type TreePath a = [TreePos Full a -> TreePos Full a]
firstChild' = fromJust . firstChild
parent' = fromJust . parent
prev' = fromJust . prev
next' = fromJust . next
-- Determine the path from the root of the tree to the cursor.
pathToMe :: TreePos Full NodeData -> TreePath NodeData
pathToMe t | isRoot t = []
| isFirst t = firstChild' : pathToMe (parent' t)
| otherwise = next' : pathToMe (prev' t)
-- Mark a tree as invalid, but leave the cursor in the same place.
invalidate :: TreePos Full NodeData -> TreePos Full NodeData
invalidate t = foldr ($) (setLabel (Left False) (root t)) (pathToMe t)
-- Set a node's internal data.
setData :: Int -> TreePos Full NodeData -> TreePos Full NodeData
setData = (invalidate . ) . setLabel . Right
main = let tree1 = Node (Left True) [Node (Right 1) [], Node (Right 2) []]
Just cursor = firstChild (fromTree tree1)
tree2 = setData 3 cursor
in do putStrLn (drawTree (fmap show tree1))
putStrLn (drawTree (fmap show (toTree tree2)))
putStrLn $ "Cursor at "++show (label tree2)
输出:
Left True
|
+- Right 1
|
`- Right 2
Left False
|
+- Right 3
|
`- Right 2
Cursor at Right 3
答案 3 :(得分:2)
我对Haskell没有多少经验,但据我所知,在纯函数式语言中不可能在参考图中使用圆圈。这意味着:
最重要的是,我不会尝试使用Java(或任何其他命令式语言)算法并尝试将其转换为Haskell。相反,尝试找到一个更实用的算法(甚至可能是一个不同的数据结构)来解决问题。
修改强>
从您的澄清中,您是否需要使更改的对象的直接父级或层次结构中的所有祖先无效,并不完全清楚,但这实际上并不重要。由于使对象无效基本上意味着更改它并且这是不可能的,因此您基本上必须创建该对象的修改副本,然后您必须使其父指向它,因此您必须为此创建一个新对象。这一直持续到你到达root。如果你有一些递归来遍历树以便“修改”你的对象,那么你可以在离开递归的路上重新创建从该对象到根的路径。
希望有道理。 :■
*正如jberryman的评论中所指出的,在其他答案中,可以使用延迟评估在Haskell中创建循环引用图。
答案 4 :(得分:0)
查看使用Functor
类型的Maybe
实例。
例如,您的问题可能是这样的:您希望将元素插入到二叉树中,但如果尚未存在则 。你可以这样做:
data Tree a = Node a (Tree a) (Tree a)
| Tip
maybeInsert :: a -> Tree a -> Maybe (Tree a)
maybeInsert a Tip = Just $ Node a Tip Tip
maybeInsert a (Node a' l r)
| a == a' = Nothing
| a < a' = fmap (\l'-> Node a' l' r) (maybeInsert a l)
| a > a' = fmap (\r'-> Node a' l r') (maybeInsert a r)
因此,如果我们发现元素已经存在,函数将返回Nothing
,或者返回Just
插入元素的新树。
希望这与你想做的事情有关。
答案 5 :(得分:0)
不能懒惰照顾确保验证不会经常发生吗?这样,您就不需要存储m_valid
字段。
例如,如果您只对保存进行验证,那么您可以将对象编辑为您的心灵内容,而无需一直重新验证;只有当用户按下“保存”按钮时才会计算validateDoc
的值。由于我不确定你的有效手段和你需要什么的概念,我可能完全是标记。
Untried&amp;不完整的代码:
data Document = Document { subDocs :: [SubDoc] }
data SubDoc = SubDoc { content :: String }
addSubDoc :: SubDoc -> (Document -> Document)
addSubDoc = error "not yet implemented: addSubDoc"
modifySubDoc :: Int -> (SubDoc -> SubDoc) -> (Document -> Document)
modifySubDoc = error "not yet implemented: modifySubDoc"
validateDoc :: Document -> Bool
validateDoc = all validateSubDoc . subDocs
validateSubDoc :: SubDoc -> Bool
validateSubDoc = not . null . contents
我假设文档的整体有效性仅取决于子文档(通过确保它们包含非空字符串来模拟此处)。
顺便说一下,我想你忘记了a.addChild(b);
中的main
。