我正在努力在Haskell中实现UCT算法,这需要大量的数据杂耍。没有太多细节,它是一个模拟算法,在每个“步骤”,基于一些统计属性选择搜索树中的叶节点,在该叶子上构造新的子节点,并且对应于新叶及其所有祖先都会更新。
鉴于所有这些杂耍,我并不是非常敏锐,无法弄清楚如何使整个搜索树成为一个不错的不可变数据结构({3}}。相反,我一直在玩ST
monad,创建由可变STRef
s组成的结构。一个人为的例子(与UCT无关):
import Control.Monad
import Control.Monad.ST
import Data.STRef
data STRefPair s a b = STRefPair { left :: STRef s a, right :: STRef s b }
mkStRefPair :: a -> b -> ST s (STRefPair s a b)
mkStRefPair a b = do
a' <- newSTRef a
b' <- newSTRef b
return $ STRefPair a' b'
derp :: (Num a, Num b) => STRefPair s a b -> ST s ()
derp p = do
modifySTRef (left p) (\x -> x + 1)
modifySTRef (right p) (\x -> x - 1)
herp :: (Num a, Num b) => (a, b)
herp = runST $ do
p <- mkStRefPair 0 0
replicateM_ 10 $ derp p
a <- readSTRef $ left p
b <- readSTRef $ right p
return (a, b)
main = print herp -- should print (10, -10)
显然,这个特殊的例子在不使用ST
的情况下编写起来要容易得多,但希望很清楚我要去哪里...如果我将这种风格应用到我的UCT用例中,这是错误的吗?
有人在几年后问过Okasaki,但我认为我的问题有点不同......我在使用monads在适当时封装可变状态没有问题,但它是“适当的时候”子句得到了我。我担心我会过早地恢复面向对象的思维模式,在那里我有一堆带有getter和setter的对象。不完全是惯用的Haskell ......
另一方面,如果 是针对某些问题的合理编码风格,我想我的问题就变成:有没有任何众所周知的方法可以保持这种代码的可读性和可维护性?我有点被所有明确的读写所淹没,尤其是必须从STRef
monad中基于ST
的结构转换为外部的同构但不可变的结构。
答案 0 :(得分:40)
我不太使用ST,但有时它只是最好的解决方案。这可以在许多情况下:
当我使用ST(和其他monad)时,我尝试遵循这些一般准则:
STRef s (Map k [v])
这样的东西。地图本身正在变异,但大部分繁重的工作都是纯粹的。IORef
和STRef
替换所有IO
和ST
s比编写手工编码的哈希表实现要容易得多,并且可能更快。最后一点 - 如果您在显式读写方面遇到问题,则有ways around it。
答案 1 :(得分:11)
使用不同算法的变异和算法的算法。有时候从前者到后者的翻译有一个明确的界限,有时是一个困难的翻译,有时只有一个不能保留复杂性的界限。
本文的一篇文章向我揭示了我认为它不会突然变得必不可少 - 所以我认为可以开发出一种潜在的非常漂亮的懒惰函数算法。但它与描述的算法不同但是相关的算法。
下面,我描述了一种这样的方法 - 不一定是最好的或最聪明的,但非常简单:
这是我理解的设置 - A)构建分支树B)然后将支付从叶子推回到根,然后在任何给定步骤指示最佳选择。 但是这是昂贵的,所以相反,只有树的一部分以非确定的方式探索到叶子。此外,对树的每次进一步探索都取决于之前探索中学到的东西。
因此我们构建代码来描述“阶段式”树。然后,我们有另一个数据结构来定义部分探索的树以及部分奖励估计。然后我们有randseed -> ptree -> ptree
函数给出一个随机种子和一个部分探索的树,开始进一步探索树,随时更新ptree结构。然后,我们可以在空种子ptree上迭代此函数,以获得ptree中越来越多的采样空间的列表。然后我们可以走这个列表,直到满足一些指定的截止条件。
所以现在我们已经从一个算法混合到三个不同的步骤 - 1)建立整个状态树,懒洋洋地,2)用一些结构的样本更新一些部分探索,3)决定何时我们收集了足够的样本。
答案 2 :(得分:9)
使用ST是合适的时候真的很难说。我建议你用ST而不用ST(不一定按顺序)。保持非ST版本简单;使用ST应该被看作是一种优化,在你知道需要它之前你不想这样做。
答案 3 :(得分:2)
我必须承认我无法读取Haskell代码。但是如果你使用ST来改变树,那么你可以用一个不可变树替换它而不会损失太多,因为:
您必须改变新叶子上方的每个节点。不可变树必须替换修改节点上方的所有节点。因此,在这两种情况下,触摸的节点都是相同的,因此您不会获得任何复杂性。
例如Java对象创建比变异更昂贵,所以也许你可以通过使用变异在Haskell中获得一些。但我不确定这一点。但是,由于下一点,小幅上涨不会给你带来太大的收益。
新叶的评估可能比更新树要昂贵得多。至少在计算机Go中就是UCT的情况。
答案 4 :(得分:2)
使用ST monad通常(但不总是)作为优化。对于任何优化,我都应用相同的程序:
我所知道的另一个用例是作为州monad的替代。关键的区别在于,对于状态monad,存储的所有数据的类型以自上而下的方式指定,而对于ST monad,它是自下而上指定的。有些情况下这很有用。