如何通过 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 中构建一个不确定的状态单子?

如何在 Haskell 平台中安装具有分析支持的 ghc 和 base

如何使用 ivy api 以编程方式为缓存中的模块构建路径?

当用户不在 Yesod/Haskell 的数据库中时如何重定向到特殊页面

具有haskell中的多参数函数的延迟过滤器