【问题标题】:How to create a tree of random values in Haskell?如何在 Haskell 中创建随机值树?
【发布时间】:2020-11-10 04:04:21
【问题描述】:

我一直在努力学习 state monad。我正在使用树,定义如下...... data Tree a = Unary (Tree a) | Binary (Tree a) (Tree a)| Ternary (Tree a) (Tree a) (Tree a) | Leaf a

我目前正在尝试做的是使用类型签名 randomize :: Tree a -> Tree Int 创建一个函数,它返回一棵树,其中每个叶子 (Leaf a) 被叶子 (Leaf 0) 或 (叶 1) 等概率。

我之前写了一个函数,我称为 label :: Enum b => Tree a -> b -> Tree b,它遍历一个 Tree 并将每个 Leaf a 替换为一个 Leaf b,其中每次访问一个叶子时 b 都会递增。定义如下

label:: Enum b => Tree a -> b -> Tree b
label tree b = evalState (mapSucc (\s -> (s, succ s)) tree) b where 
    mapSucc f (Leaf a) = Leaf <$> state f
    mapSucc f (Unary t1) = Unary <$> mapSucc f t1
    mapSucc f (Binary t1 t2) = Binary <$> mapSucc f t1 <*> mapSucc f t2
    mapSucc f (Ternary t1 t2 t3) = Ternary <$> mapSucc f t1 <*> mapSucc f t2 <*> mapSucc f t3

这些似乎是非常相似的问题,你在每个问题中编织状态,你只是产生不同的值。我试过了……

randomize tree = evalState ( mapRandom (randomR (0,1)) tree) newStdGen  where 
    mapRandom f (Leaf a) = Leaf <$> state f
    mapRandom f (Unary t1) = Unary <$> mapRandom f t1
    mapRandom f (Binary t1 t2) = Binary <$> mapRandom f t1 <*> mapRandom f t2
    mapRandom f (Ternary t1 t2 t3) = Ternary <$> mapRandom f t1 <*> mapRandom f t2 <*> mapRandom f t3

但是编译器给了我以下内容

state.hs:55:41: error:
    • No instance for (RandomGen (IO StdGen))
        arising from a use of ‘randomR’
    • In the first argument of ‘mapRandom’, namely ‘(randomR (0, 1))’
      In the first argument of ‘evalState’, namely
        ‘(mapRandom (randomR (0, 1)) tree)’
      In the expression:
        evalState (mapRandom (randomR (0, 1)) tree) newStdGen

我认为 Int 是 random 的一个实例,所以我不确定该怎么做才能获得 Random 的实例,如果我的思维过程走在正确的道路上,我是否确实做到了。如果我要获得一个随机实例,我的解决方案会起作用吗?我想我不确定我的缺点是否源于不知道如何使用 System.Random 或者我不了解我需要的函数类型。我花了很多时间尝试使随机化工作,但无济于事。任何有助于理解的帮助将不胜感激。

【问题讨论】:

  • 您为这两种解决方案实现的通用机制是 Traversable 类型类。我在stackoverflow.com/a/41942347/625403 回答了一个类似的问题——你可以考虑单独实现 Traversable,然后这个问题就变得很简单了。
  • 看traverse的定义,我基本上已经写好了traverse,label和randomize都是traverse,定义里有特定的函数。标签工作得很好,但随机化不想编译。我已经尝试了它的许多变体,但是用 randomR 遍历似乎给我带来了问题。这不是我不理解的遍历,而是如何使用一个可以给我随机值的函数。

标签: haskell random tree


【解决方案1】:

从编译器错误消息来看,问题似乎是由于您的随机数生成器包含在 IO monad 中,因为您使用了函数 newStdGen

您可以改用函数mkStdGen 来获得解包的生成器。该函数将一个 Int 类型的参数作为生成器的种子。

例如,这段代码编译:

randomize :: (Random a, Num a) => Int -> Tree a -> Tree a
randomize seed tree = evalState ( mapRandom (randomR (0,1)) tree) (mkStdGen seed)  where 
    mapRandom f (Leaf a) = Leaf <$> state f
    mapRandom f (Unary t1) = Unary <$> mapRandom f t1
    mapRandom f (Binary t1 t2) = Binary <$> mapRandom f t1 <*> mapRandom f t2
    mapRandom f (Ternary t1 t2 t3) = Ternary <$> mapRandom f t1
                                     <*> mapRandom f t2 <*> mapRandom f t3

作为奖励,您可以随意复制相同的随机数序列(通过再次传递相同的种子),这是newStdGen 不提供的。

函数newStdGen 使用系统时钟来生成种子,因此需要涉及 IO monad。

附录:

如何避免重复遍历树的代码

上面的函数randomize 有效。然而,它并不完全令人满意:它涉及树遍历的算法,它决定使用哪个范围的值,以及哪种类型的随机数生成器。所以它似乎在计算机编程中被称为Single Responsibility Principle (SRP) 的东西失败了。

有一天可能需要不同的范围。此外,您可以假设Threefish 随机数生成器比标准 Haskell 具有更好的统计特性,并希望使用它的Haskell implementation

阻力最小的路径是克隆randomize 的代码并将mkStdGen 替换为mkTFGen。但是此时,树遍历代码会重复。并且有许多潜在的重复。我们应该找到更好的方法。

一般问题是使用有状态映射生成初始树的新版本。到目前为止,我们忽略了初始树中的 ,但这只是针对随机输出树的特殊情况。

通常,所需转换函数的类型签名必须是:

statefulTreeMap :: (a -> s -> (b,s))  ->  s  ->  Tree a  ->  (Tree b, s)

其中s状态 的类型。在randomize 的情况下,状态只是随机数生成器的(当前状态)。

您可以轻松地手动编写statefulTreeMap 的代码,使用如下子句:

statefulTreeMap step st0 (Binary tra1 tra2) =
    let  (trb1, st1) = statefulTreeMap step st0 tra1
         (trb2, st2) = statefulTreeMap step st1 tra2
    in  (Binary trb1 trb2 , st2)

但这并不是最Haskellish的方式。

事实证明,这与mapAccumL 库函数非常相似。 Haskell 语言库使mapAccumL 可用于属于Traversable 类型类的任何实体。请注意,@amalloy 在其中一个 cmets 中提到了 Traversable 类型类。

所以我们可以尝试让我们的Tree 类型为Traversable 的实例,然后使用函数mapAccumL

这可以通过显式提供函数traverse的代码来完成:

instance Traversable Tree  where
    traverse fn (Unary ta)          =  Unary    <$>  (traverse fn ta)
    traverse fn (Binary ta tb)      =  Binary   <$>  (traverse fn ta) <*> (traverse fn tb)
    traverse fn (Ternary ta tb tc)  =  Ternary  <$>  (traverse fn ta) <*> (traverse fn tb) <*> (traverse fn tc)
    traverse fn (Leaf a)            =  Leaf <$> fn a

但这甚至没有必要。相反,可以召唤编译器重炮(至少在足够近的版本中),并通过启用DeriveTraversable 语言扩展来让它生成traverseTree 版本:


{-#  LANGUAGE  DeriveFunctor         #-}
{-#  LANGUAGE  DeriveFoldable        #-}
{-#  LANGUAGE  DeriveTraversable     #-}

import qualified  Data.Tuple        as  T
import qualified  Data.Traversable  as  TR
import  System.Random
import qualified  System.Random.TF  as  TF
import  Control.Monad.State


data Tree a = Unary (Tree a)  |  Binary (Tree a) (Tree a)  |
              Ternary (Tree a) (Tree a) (Tree a)  |  Leaf a
                deriving  (Eq, Show, Functor, Foldable, Traversable)

然后我们可以通过在我们刚刚免费获得的mapAccumL 函数周围放置一些管道代码来获得目标statefulTreeMap 函数的通用版本:

-- general solution for any Traversable entity:
statefulMap :: Traversable tr => (a -> s -> (b,s)) -> s -> tr a -> (tr b, s)
statefulMap step st0 tra =
    let  fn = \s y -> T.swap (step y s)
         p  = TR.mapAccumL fn st0 tra -- works in reverse if using mapAccumR
    in
         T.swap p

我们可以立即将其专门用于Tree 对象:

statefulTreeMap :: (a -> s -> (b,s)) -> s -> Tree a -> (Tree b, s)
statefulTreeMap = statefulMap

至此我们几乎完成了。我们现在可以通过提供更多管道代码来编写多个版本的randomize

-- generic random tree generation, with range and generator as external parameters:
randomize2 :: (RandomGen gt, Random b, Num b)  =>  (b,b) -> gt -> Tree a -> Tree b
randomize2 range gen tra =
    let  step = (\a g -> randomR range g)        -- leftmost parameter ignored
    in   fst $ statefulTreeMap  step  gen  tra   -- drop final state of rng

-- version taking just a seed, with output range and generator type both hardwired:
randomize3 :: (Random b, Num b) => Int -> Tree a -> Tree b
randomize3 seed tra  =  let  rng   = TF.mkTFGen seed
                             range = (0,9)
                        in   randomize2  range  rng  tra

测试代码:

main = do
    let  seed = 4243
         rng0 = TF.mkTFGen seed
         tr1  = Ternary  (Ternary (Leaf 1) (Leaf 2) (Leaf 3))
                         (Leaf (4::Integer))
                         (Binary  (Leaf 12) (Leaf 13))

         tr11 = (randomize    seed       tr1)  :: Tree Integer
         tr12 = (randomize2  (0,9) rng0  tr1)  :: Tree Integer
         tr13 = (randomize3   seed       tr1)  :: Tree Integer

    putStrLn $ "tr1   =  " ++ (show tr1) ++ "\n"
    putStrLn $ "tr11  =  " ++ (show tr11)
    putStrLn $ "tr12  =  " ++ (show tr12)
    putStrLn $ "tr13  =  " ++ (show tr13)

    putStrLn $ "tr11 == tr12  =  " ++ (show (tr11 == tr12))
    putStrLn $ "tr11 == tr13  =  " ++ (show (tr11 == tr13))

程序输出:

tr1   =  Ternary (Ternary (Leaf 1) (Leaf 2) (Leaf 3)) (Leaf 4) (Binary (Leaf 12) (Leaf 13))

tr11  =  Ternary (Ternary (Leaf 9) (Leaf 6) (Leaf 0)) (Leaf 3) (Binary (Leaf 2) (Leaf 6))
tr12  =  Ternary (Ternary (Leaf 9) (Leaf 6) (Leaf 0)) (Leaf 3) (Binary (Leaf 2) (Leaf 6))
tr13  =  Ternary (Ternary (Leaf 9) (Leaf 6) (Leaf 0)) (Leaf 3) (Binary (Leaf 2) (Leaf 6))
tr11 == tr12  =  True
tr11 == tr13  =  True

因此,事实上,我们消除了对任何显式树遍历代码的需求。

旁注:

当然,statefulTreeMap 函数可以用于与伪随机无关的任务。例如,我们可能希望为 Tree 对象的元素赋予连续的数字:

enumerate :: Tree a -> Tree (a, Int)
enumerate = fst . (statefulTreeMap  (\a rs -> ((a, head rs), tail rs))  [0..])

ghci下测试:

 λ> 
 λ> enumerate tr1
Ternary (Ternary (Leaf (1,0)) (Leaf (2,1)) (Leaf (3,2))) (Leaf (4,3)) (Binary (Leaf (12,4)) (Leaf (13,5)))
 λ> 

【讨论】:

  • 谢谢!是的,我不知何故错过了newStdGen 是一个 IO 单子。使用mkStdGen seed 基本上是我想要的并且会正常工作。
  • @marcushio - 为了完整起见,我刚刚添加了解释如何利用 Traversable 类型类的内容,正如 amalloy 在其中一个 cmets 中所暗示的那样。
【解决方案2】:

您的尝试很接近。问题是newStdGenIO StdGen,但您需要StdGen。要解决此问题,请将 evalState ( mapRandom (randomR (0,1)) tree) newStdGen 更改为 evalState ( mapRandom (randomR (0,1)) tree) <b>&lt;$&gt;</b> newStdGen。请注意,它将是 randomize :: Tree a -&gt; IO (Tree Int) 而不是 randomize :: Tree a -&gt; Tree Int,但您无法避免更改类型签名(您唯一的其他选择是将 StdGen 设为参数,这也会更改它)。

【讨论】:

    猜你喜欢
    • 2013-01-30
    • 2020-09-28
    • 1970-01-01
    • 1970-01-01
    • 2014-03-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多