ST Monad ==代码味道?

我正在实施Haskell中的UCTalgorithm,这需要相当数量的数据杂耍。 没有太多的细节,这是一个模拟algorithm,在每个“步骤”中,根据一些统计属性selectsearch树中的叶节点,在该叶构build新的子节点,并且与新叶和所有的祖先更新。

考虑到所有这些杂耍,我并不十分清楚如何让整个search树成为一个不可变的数据结构。 相反,我一直在玩ST monad,创build由可变STRef组成的结构。 一个人为的例子(与UCT无关):

 import Control.Monad import Control.Monad.ST import Data.STRef data STRefPair sab = STRefPair { left :: STRef sa, right :: STRef sb } mkStRefPair :: a -> b -> ST s (STRefPair sab) mkStRefPair ab = do a' <- newSTRef a b' <- newSTRef b return $ STRefPair a' b' derp :: (Num a, Num b) => STRefPair sab -> 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用例中,那是错误的吗?

有人在几年前提出了一个类似的问题 ,但是我认为我的问题有点不同……我没有问题,在适当的时候使用monads来封装可变状态,但是这是“适当的时候”从句。 我担心我会过早地回到面向对象的思维模式,在那里我有一堆有getter和setter的对象。 不完全是惯用的Haskell …

另一方面,如果对于某些问题一种合理的编码风格,我想我的问题是:有没有什么知名的方法来保持这种代码的可读性和可维护性? 我被所有显式的读写操作STRef了,特别是从ST monad中的基于STRef的结构翻译到STRef的同构但不可变的结构。

我不太用ST,但有时候这只是最好的解决scheme。 这可以在许多情况下:

  • 已经有众所周知的,有效的方法来解决问题。 Quicksort就是一个很好的例子。 它以其速度和就地行为而闻名,纯代码无法很好地模仿它。
  • 你需要严格的时间和空间的界限。 特别是对于懒惰的评估(Haskell甚至没有指定是否有懒惰的评估,只是它是非严格的),你的程序的行为可能是非常不可预测的。 是否有内存泄漏可能取决于是否启用某个优化。 这与命令式代码有很大不同,命令式代码有一组固定的variables(通常是)和定义的评估顺序。
  • 你有最后期限。 虽然纯粹的风格几乎总是更好的练习和更清晰的代码,但如果你习惯于尽快编写代码并且很快就需要代码,那么启动命令并在以后转向function是一个完全合理的select。

当我使用ST(和其他单子)时,我尝试遵循这些一般准则:

  • 经常使用适用的样式。 这使得代码更容易阅读,如果切换到不可变的版本,转换更容易。 不仅如此,应用风格也更加紧凑。
  • 不要只用ST。 如果你只在ST编程,结果将不会比一个巨大的C程序更好,可能更糟,因为明确的读写。 相反,散布纯粹的Haskell代码应用。 我经常发现自己使用像STRef s (Map k [v]) 。 地图本身正在发生变化,但是大部分的繁重工作都是纯粹完成的。
  • 如果你不需要重做库, 为IO编写的许多代码可以干净地,机械地转换为ST。 在IORef中用STRefIORef代替所有的IORef要比编写一个手工编码的哈希表实现更容易,而且可能也更快。

最后一个注意事项 – 如果您在显式读取和写入时遇到问题,可以采用其他方法 。

使用变异的algorithm和不是不同algorithm的algorithm。 有的时候,从前者到后者有一个严格的边界保留的翻译,有时是一个困难的,有时只有一个不保留复杂的边界。

这篇论文的一个小贴子告诉我,我不认为这是突变的必要用法,所以我认为可能会开发一个潜在的非常漂亮的懒惰函数algorithm。 但是这将是一个不同但相关的algorithm。

在下面,我描述了一个这样的方法 – 不一定是最好或最聪明的,但非常简单:

这是设置一个我的理解 – A)一个分支树构造B)支付然后推回从叶子到根,然后指出在任何给定的步骤的最佳select。 但这是昂贵的,相反,只有部分树木是以非确定的方式探索叶子的。 此外,对树的每一次进一步的探索都是由以前的探索中所学到的。

所以我们build立代码来描述“stage-wise”树。 然后,我们有另一个数据结构来定义一个部分探索的树和部分奖励估计值。 然后,我们有一个randseed -> ptree -> ptree的函数,给定一个随机种子和一个部分探索的树,开始进一步探索树,更新ptree结构。 然后,我们可以在一个空的种子树上迭代这个函数来获得ptree中越来越多的采样空间的列表。 然后我们可以走这个列表,直到满足某个指定的截止条件。

所以,现在我们已经从一种algorithm将所有的东西都混合在一起,到三个不同的步骤:1)懒惰地构build整个状态树; 2)用一些结构抽样更新一些局部的探索; 3)决定我们什么时候收集足够的样本。

使用ST适当的时候可能很难分辨出来。 我build议你做ST和ST没有(不一定按顺序)。 保持非ST版本简单; 使用ST应该被看作是一种优化,你不想这样做,直到你知道你需要它。

我不得不承认,我不能读取Haskell代码。 但是如果你使用ST来改变树,那么你可以用一棵不变的树代替它而不会损失太多,因为:

对于可变和不可变树同样复杂

你必须改变新叶子上的每个节点。 不可变树必须replace修改节点上的所有节点。 所以在这两种情况下,被触摸的节点是相同的,因此你不会在复杂性上获得任何东西。

例如,Java对象的创build比变异更昂贵,所以也许你可以通过使用变异在Haskell中获得一些。 但是这个我不确定。 但是由于下一点,小小的收益并不会给你带来太多的收益。

更新树大概不是瓶颈

新叶的评估可能比更新树要昂贵得多。 至less在计算机Go中,UCT就是这种情况。

ST monad的使用通常(但不总是)作为优化。 对于任何优化,我使用相同的程序:

  1. 写没有它的代码,
  2. configuration文件和识别瓶颈,
  3. 逐渐重写瓶颈并testing改进/回归,

我知道的另一个用例是作为国家monad的替代品。 关键的区别在于,对于monad,所有存储的数据的types都是以自顶向下的方式指定的,而对于ST monad,则是以自下而上的方式指定的。 有些情况下这是有用的。

Interesting Posts