Haskell 我是否需要采取明确的措施来促进与持久数据结构的共享?
我来自一个命令式的背景,正在尝试实现一个简单的不相交集(“联合查找”)数据结构,以获得在Haskell中创建和修改(持久性)数据结构的一些实践。目标是有一个简单的实现,但我也关心效率,我的问题与此相关 首先,我使用union by rank创建了一个不相交的集合林实现,并从定义“点”的数据类型开始:Haskell 我是否需要采取明确的措施来促进与持久数据结构的共享?,haskell,data-structures,disjoint-sets,union-find,Haskell,Data Structures,Disjoint Sets,Union Find,我来自一个命令式的背景,正在尝试实现一个简单的不相交集(“联合查找”)数据结构,以获得在Haskell中创建和修改(持久性)数据结构的一些实践。目标是有一个简单的实现,但我也关心效率,我的问题与此相关 首先,我使用union by rank创建了一个不相交的集合林实现,并从定义“点”的数据类型开始: data Point = Point { _value :: Int , _parent :: Maybe Point , _rank :: Int } deriving Sh
data Point = Point
{ _value :: Int
, _parent :: Maybe Point
, _rank :: Int
} deriving Show
不相交的集合林是具有Int的IntMap
→ 点
映射:
type DSForest = IntMap Point
empty :: DSForest
empty = I.empty
单例集只是从其值x到值为x、无父项且秩为1的点的映射:
makeSet :: DSForest -> Int -> DSForest
makeSet dsf x = I.insert x (Point x Nothing 0) dsf
现在,有趣的部分是union。此操作将通过将另一个点设置为其父点(在某些情况下更改其秩)来修改点。如果点
s'的秩不同,点
只需“更新”(创建一个新点)即可使其父点指向另一个点。在它们相等的情况下,将创建一个新的点
,其秩增加一:
union :: DSForest -> Int -> Int -> DSForest
union dsf x y | x == y = dsf
union dsf x y =
if _value x' == _value y'
then dsf
else case compare (_rank x') (_rank y') of
GT -> I.insert (_value y') y'{ _parent = Just x' } dsf
LT -> I.insert (_value x') x'{ _parent = Just y' } dsf
-- 1) increase x's rank by one:
EQ -> let x'' = x'{ _rank = _rank x' + 1 }
-- 2) update the value for x's rank to point to the new x:
dsf' = I.insert (_value x'') x'' dsf
-- 3) then update y to have the new x as its parent:
in I.insert (_value y') y'{ _parent = Just x'' } dsf'
where x' = dsf ! findSet dsf x
y' = dsf ! findSet dsf y
现在,对于我真正的问题,如果在EQ
案例中,我做了以下操作:
EQ -> let dsf' = I.insert (_value x') x'{ _rank = _rank x' + 1} dsf
in I.insert (_value y') y'{ _parent = Just x'{ _rank = _rank x' + 1 }} dsf'
也就是说,首先插入一个新的点
x,其秩增加,然后让y'
的父项成为一个新的点
x,其秩增加,这是否意味着它们不再指向内存中相同的点
(这有关系吗?在使用/创建持久数据结构时,我是否应该担心这些问题?)
为了完整起见,这里是findSet
:
findSet :: DSForest -> Int -> Int
findSet dsf' x' = case _parent (dsf' ! x') of
Just (Point v _ _) -> findSet dsf' v
Nothing -> x'
(也欢迎对本规范的效率和设计提出一般性意见。)
这是否意味着它们不再指向记忆中的同一点
我认为您不应该关心这个问题,因为这只是针对不可变值的运行时系统(也称为Haskell的RTS)的一个实现细节
至于其他建议,我想说让函数findSet
返回点本身,而不是键,因为这样可以消除在union
中的查找
findSet :: DSForest -> Int -> Point
findSet dsf' x' = case _parent pt of
Just (Point v _ _) -> findSet dsf' v
Nothing -> pt
where
pt = (dsf' ! x')
在union
函数中进行适当的更改。共享是一个编译器的事情。当它识别公共子表达式时,编译器可能会选择用内存中的同一个对象来表示它们。但是,即使您使用这样的编译器开关(如-fno cse
),它也没有义务这样做,而且两者可能是相同的(在没有开关的情况下,通常是)由内存中两个不同但值相等的对象表示。Re:
当我们为某个对象命名并使用该名称两次时,我们(合理地)希望它在内存中表示同一对象。但是编译器可能会选择复制它,并在两个不同的使用站点中使用两个单独的副本,尽管不知道是否会这样做。但是可能。Re:
另见:
这里有几个列表生成函数的示例,来自上面的最后一个链接。它们依赖于编译器,不复制任何内容,即按照需要调用lambda演算操作语义的预期共享任何命名对象(如nponeccop在注释中解释的),并且不单独引入任何额外共享以消除公共子表达式:
共享固定点组合器,创建循环:
修复f=x,其中x=fx
非共享不动点组合器,创建伸缩多级链(即正则递归链)
\u Y f=f(\u Y f)
两级组合-回路和馈电
\u 2 f=f(固定f)
第一点意见:不相交集联合查找数据结构很难用纯函数的方式很好地完成。如果您只是想练习持久数据结构,我强烈建议您从简单的结构开始,如二进制搜索树
现在,考虑到一个问题,考虑一下你的FINSET函数。它不实现路径压缩,也就是说,它不会使所有的节点沿着根点的路径直接到达根目录。要做到这一点,你需要更新DS森林中的所有这些点,这样你的函数就会返回(int,DS森林)或者(点,DS森林)。。在monad中执行此操作以处理传递DSForest的所有管道比手动传递该林更容易
但现在是第二个问题。假设您如前所述修改findSet。它仍然不能完全满足您的需要。特别是,假设您有一个链,其中2是1的子级,3是2的子级,4是3的子级。现在您在3上执行findSet。这将更新3的点,使其父级是1而不是2。但4的父级仍然是旧的3点,其父点是2。这可能没什么大不了的,因为看起来你从来没有对父点做过任何事情,除了拉出它的值(在findSet中)。但是你从来没有对父点做过任何事情,除了拉出它的值,这一事实告诉我,它应该是一个可能整数,而不是一个可能点
让我重复并扩展我在开始时所说的内容。不相交集是一种特别难以用功能性/持久性方式处理的数据结构,因此我强烈建议从更简单的树结构开始,如二元搜索树或左撇子堆,甚至抽象语法树。这些结构具有所有访问都可以访问的属性通过根——也就是说,您总是从根开始,沿着树向下走,以到达正确的位置。此属性使持久数据结构的标志性共享变得更加容易
不相交集数据结构没有该属性。而不是始终从根开始