我希望在Haskell中重构一个图的发生结构,该结构由其广度优先遍历的输出给出。明确地说,输出包括一个根节点和一个邻域列表(邻域是一个标记为新或旧(=已访问)的顶点列表),其中每个邻域对应于尚未分配到邻域的最小顶点。
在任何命令式语言中,我会使用队列来解决这个问题:
数据类型“Output”包含已解析的BFS输出,包括根顶点和邻域列表。BFSNode对应于BFS树中的一个节点,该节点属于新访问的顶点之一,或者属于已经被访问过并因此由其唯一编号引用的旧顶点。请注意,解析过程工作正常且消耗的内存很少。
我的目标是将“Output”转换为数据类型“Graph”,它由顶点列表和关联列表组成。
以下是我实现的简化版本:
"nbs"是邻居列表,"qs"是队列。函数"nodeNr"从顶点中提取唯一标识号,"isNew"测试顶点是否为新的,"unNew"从数据类型"BFSNode"中解压新的顶点。 编辑2:我现在认为我已经定位了问题所在。也许这与我的转换过程实现无关。我的失败之处在于使用内置函数"read"从文件中读取数据类型"Output"。我现在意识到,Haskell在读取大文件时存在问题。即使只是读取整数列表,例如。
如果文件“test”足够大,程序将耗尽内存。我现在明白了,这种解析数据的方式不好,因为“read”会在显示任何输出之前将所有数据加载到内存中,但我仍然不明白为什么它会占用16GB的RAM,即使文件数量还不到500MB。你有没有想过“read”出了什么问题?在你的机器上Haskell是否表现出相同的行为?
编辑3:现在我实现了一个基于流的解析函数“readOutput”,它接受一个字符串并返回数据类型“Output”。这个函数是惰性的,所以当我调用它时,我立即得到一个输出。但是当我将它与我的转换函数“readTree”组合时(这显然是尾递归的),我根本没有得到任何输出,并且内存使用量像往常一样增加。我做错了什么?
编辑4:编辑3中的问题来自某些强制执行,我现在已经删除了它们。
在任何命令式语言中,我会使用队列来解决这个问题:
Input: root vertex r, list of neighborhoods L
(1) Put r into the empty queue Q
(2) if Q is empty then STOP
(3) extract the first vertex v of Q
(4) extract the first neighborhood N of L
(5) append the unvisited vertices of N to Q
(6) remove the markings (new/old) of the nodes of N and assign v to N
(7) goto (2)
我尝试在Haskell中实现这个朴素算法(使用列表或使用Data.Sequence作为队列),但是ghci总是会用光内存。这不应该发生,因为虽然输入由300MB数据组成,但16GB的RAM显然足够。
因此,朴素实现似乎导致了内存泄漏。你会如何在Haskell中实现这个算法?
编辑: 这里是我使用的(稍微简化的)数据类型:
data Output = Out !Vertex ![[BFSNode]]
data Vertex = Vertex Integer SomeMoreComplexData
data BFSNode = New Vertex | Old Integer
data Graph = ![Vertex] ![(Integer,[Integer])]
数据类型“Output”包含已解析的BFS输出,包括根顶点和邻域列表。BFSNode对应于BFS树中的一个节点,该节点属于新访问的顶点之一,或者属于已经被访问过并因此由其唯一编号引用的旧顶点。请注意,解析过程工作正常且消耗的内存很少。
我的目标是将“Output”转换为数据类型“Graph”,它由顶点列表和关联列表组成。
以下是我实现的简化版本:
readTree :: [[BFSNode]] -> Seq Integer -> Graph
readTree [] _ = Graph [] []
readTree (nb:nbs) qs =
let (i :< qs') = viewl qs
newVs = fromList $! map nodeNr . filter isNew $ nb
(Graph vs adj) = readTree nbs $ qs' >< newVs
in Graph (map unNew (filter isNew nb) ++ vs) ((i,nub $ map nodeNr nb):adj)
"nbs"是邻居列表,"qs"是队列。函数"nodeNr"从顶点中提取唯一标识号,"isNew"测试顶点是否为新的,"unNew"从数据类型"BFSNode"中解压新的顶点。 编辑2:我现在认为我已经定位了问题所在。也许这与我的转换过程实现无关。我的失败之处在于使用内置函数"read"从文件中读取数据类型"Output"。我现在意识到,Haskell在读取大文件时存在问题。即使只是读取整数列表,例如。
main = do
txt <- readFile "test"
writeFile "test2" . show $ (read txt :: [Integer]) }
如果文件“test”足够大,程序将耗尽内存。我现在明白了,这种解析数据的方式不好,因为“read”会在显示任何输出之前将所有数据加载到内存中,但我仍然不明白为什么它会占用16GB的RAM,即使文件数量还不到500MB。你有没有想过“read”出了什么问题?在你的机器上Haskell是否表现出相同的行为?
编辑3:现在我实现了一个基于流的解析函数“readOutput”,它接受一个字符串并返回数据类型“Output”。这个函数是惰性的,所以当我调用它时,我立即得到一个输出。但是当我将它与我的转换函数“readTree”组合时(这显然是尾递归的),我根本没有得到任何输出,并且内存使用量像往常一样增加。我做错了什么?
编辑4:编辑3中的问题来自某些强制执行,我现在已经删除了它们。
ghc -O2
编译它,而不是使用GHCi。 - gsprSequence
的叶子节点添加一些严格性是值得的,例如确保在进入队列之前所有数据都被完全评估。一个顶点的实现是什么?它可以被取消装箱/强制执行吗? - daniel gratzer