如何通过 Haskell 中的弱指针缓存构建具有重复消除的无限树
Posted
技术标签:
【中文标题】如何通过 Haskell 中的弱指针缓存构建具有重复消除的无限树【英文标题】:How to build an infinite tree with duplicate elimination via cache of weak pointers in Haskell 【发布时间】:2016-09-08 03:06:21 【问题描述】:以下代码构建了一个无限树,同时创建了所有子树的缓存,这样就不会创建重复的子树。消除重复子树的基本原理来自于象棋类游戏的状态树的应用:一个人通常可以通过改变两个移动的顺序来结束相同的游戏状态。随着游戏的进行,不可访问的状态不应继续占用内存。我想我可以通过使用弱指针来解决这个问题。不幸的是,使用弱指针将我们带入 IO Monad,这似乎已经破坏了足够多/所有的惰性,以至于这段代码不再终止。
因此,我的问题是:是否可以有效地生成没有重复子树(并且不会泄漏内存)的惰性(游戏状态)树?
-# LANGUAGE RecursiveDo #-
import Prelude hiding (lookup)
import Data.Map.Lazy (Map, empty, lookup, insert)
import Data.List (transpose)
import Control.Monad.State.Lazy (StateT(..))
import System.Mem.Weak
import System.Environment
type TreeCache = Map Integer (Weak NTree)
data Tree a = Tree a [Tree a]
type Node = (Integer, [Integer])
type NTree = Tree Node
getNode (Tree a _) = a
getVals = snd . getNode
makeTree :: Integer -> IO NTree
makeTree n = fst <$> runStateT (makeCachedTree n) empty
makeCachedTree :: Integer -> StateT TreeCache IO NTree
makeCachedTree n = StateT $ \s -> case lookup n s of
Nothing -> runStateT (makeNewTree n) s -- makeNewTree n s
Just wt -> deRefWeak wt >>= \mt -> case mt of
Nothing -> runStateT (makeNewTree n) s
Just t -> return (t,s)
makeNewTree :: Integer -> StateT TreeCache IO NTree
makeNewTree n = StateT $ \s -> mdo
wt <- mkWeak n t Nothing
(ts, s') <- runStateT
(mapM makeCachedTree $ children n)
(insert n wt s)
let t = Tree (n, values n $ map getVals ts) ts
return (t, s')
children n = let bf = 10 in let hit = 2 in [bf*n..bf*n+bf+hit-1]
values n [] = repeat n
values n nss = n:maximum (transpose nss)
main = do
args <- getArgs
let n = read $ head args in
do t <- makeTree n
if length args == 1 then putStr $ show $ take (fromInteger n) $ getVals t else putStr "One argument only!!!"
【问题讨论】:
我不认为弱指针(因此IO
)是必要的。例如,Data.Seq
竭尽全力使用一些巧妙的代码来最大化内部共享:hackage.haskell.org/package/containers-0.5.7.1/docs/src/…
@cdk,我无法完全找到它在哪里/如何做到这一点。从评论“特别说明:身份特化自动进行节点共享,将结果树的内存使用减少到 /O(log n)/”,似乎暗示代码没有显式实现共享,但编译器优化对于特定的专业化,无论如何都要让它发生(这意味着对于其他情况它可能不会发生)。您能否详细解释一下 Data.Seq 如何进行共享以及这对我有什么帮助?
@hkBst,该评论具有误导性,我将尝试对其进行编辑以在下一个版本中澄清。 Identity
的编译器专业化除了改进常量因子外什么也没做。它真正的意思是与Identity
一起使用时有很多分享。
我看不出你想用弱指针做什么。如果您将游戏状态更改为指向代表所选移动的状态树根的子节点,则其他子树应该变得不可访问并被垃圾收集器丢弃。
@dfeuer,但缓存仍将包含对先前状态的引用,从而阻止垃圾回收。
【参考方案1】:
因此,我的问题是:是否可以有效地生成没有重复子树(并且不会泄漏内存)的惰性(游戏状态)树?
没有。基本上你想要做的是使用transposition tables 来记忆你的树搜索功能(例如negascout)。问题是游戏有指数级的状态,维护一个能记住状态空间所有转置的转置表是不可行的。你只是没有那么多内存。
例如,国际象棋有一个state space compexity of 10^47。这比全世界所有可用的计算机内存都要大几个数量级。当然,您可以通过不存储反射来减少此数量(国际象棋有 8 reflection symmetries)。此外,由于pruning of the game tree,其中许多转置将无法访问。然而,状态空间是如此难以处理,以至于您仍然永远无法存储每个转置。
大多数程序通常做的是他们使用一个固定大小的转置表,当两个转置哈希到相同的值时,他们使用replacement scheme 来决定哪些条目将被保留,哪些将被丢弃。这是一个权衡,因为您以必须遍历另一个转置为代价来保持您认为最有效的值(即访问次数最多或更接近根节点)。关键是,除非你来自足够先进的外星文明,否则不可能生成没有重复子树的游戏树。
【讨论】:
lazily这样做的意义在于可以通过限制遍历来限制使用的内存量。问题是您是否可以懒惰地做到这一点,并且还可以动态检测和消除重复项。 首先,懒惰不会神奇地减少你的内存需求。其次,大多数博弈树搜索算法都是深度优先搜索,无论如何只需要线性内存。第三,大多数国际象棋引擎(甚至是用 C 编写的引擎)都有惰性移动生成器(即它们一次只生成一个移动,而不是一次生成所有移动)。第四,无论您是用 C 还是 Haskell 编写程序,检测和消除重复项都需要指数级的内存。国际象棋是一个 EXPSPACE 问题,您无能为力。 第五,当然你可以同时拥有惰性表和转置表。大多数国际象棋引擎都可以。但是,转置表不能存储每个转置,因此您不能消除每个重复节点。懒惰无助于减少这种情况。懒惰不是这样运作的。 即便如此,国际象棋引擎也使用转置表,正如您自己注意到的那样...我的问题首先是一个 Haskell 问题,其次是一个国际象棋引擎问题;如果您的回答不能帮助我理解为什么我的程序不懒惰,那么它就不会回答我的问题。以上是关于如何通过 Haskell 中的弱指针缓存构建具有重复消除的无限树的主要内容,如果未能解决你的问题,请参考以下文章
Haskell Shake build:如何使用shakeShare 和/或shakeCloud 设置共享缓存文件夹?
如何在 Haskell 平台中安装具有分析支持的 ghc 和 base
如何使用 ivy api 以编程方式为缓存中的模块构建路径?