使用随机甚至MonadRandom来实现你的随机播放。这里有一些很好的答案
但这确实是可操作的。这是幕后发生的事情。
一世。
随机性是您在 Haskell 中首先遇到并且必须处理杂质的地方之一——这似乎令人反感,因为洗牌和样本看起来如此简单,并且不觉得它们应该与打印到物理屏幕或发射核武器,但通常purity == referentially transparent
和参考透明的随机性是无用的。
random = 9 -- a referentially transparent random number
所以我们需要一个关于随机性的不同想法来让它变得纯粹。
二、
用于提高可重复性(非常重要)的科学代码中的典型“作弊”是修复实验的随机种子,以便其他人可以验证每次运行代码时他们得到完全相同的结果。这正是参考透明性!让我们试试看。
type Seed = Int
random :: Seed -> (Int, Seed)
random s = (mersenneTwisterPerturb s, splitSeed s)
其中mersenneTwisterPerturb
是从Seed
s 到的伪随机映射Int
,splitSeed
是从Seed
s 到Seed
s 的伪随机映射。请注意,这两个函数都是完全确定的(并且是透明的),所以random
也是如此,但是我们可以像这样创建一个无限的、惰性的伪随机流
randomStream :: Seed -> [Int]
randomStram s = mersenneTwisterPerturb s : randomStream (splitSeed s)
同样,这个流是基于Seed
值的确定性的,但是只看到流而不看到种子的观察者应该无法预测它的未来值。
三、
我们可以使用随机整数流来打乱列表吗?当然我们可以,通过使用模运算。
shuffle' :: [Int] -> [a] -> [a]
shuffle' (i:is) xs = let (firsts, rest) = splitAt (i `mod` length xs) xs
in (head rest) : shuffle' is (firsts ++ tail rest)
或者,为了使其更加独立,我们可以预先组合我们的流生成函数来得到
shuffle :: Seed -> [a] -> [a]
shuffle s xs = shuffle' (randomStream s) xs
另一个“种子消耗”引用透明的“随机”功能。
四。
所以这似乎是一个重复的趋势。事实上,如果你浏览这个模块System.Random
,你会看到很多我们上面写的函数(我已经专门化了一些类型类)
random :: (Random a) => StdGen -> (a, StdGen)
randoms :: (Random a) => StdGen -> [a]
其中Random
是可以随机生成的事物的类型类,StdGen
是 的一种Seed
。这已经足够实际的工作代码来编写必要的改组函数了。
shuffle :: StdGen -> [a] -> [a]
shuffle g xs = shuffle' (randoms g) xs
还有一个IO
函数newStdGen :: IO StdGen
可以让我们构建一个随机种子。
main = do gen <- newStdGen
return (shuffle gen [1,2,3,4,5])
但是你会注意到一些烦人的事情:如果我们想要做出不同的随机排列,我们需要不断改变 gen
main = do gen1 <- newStdGen
shuffle gen1 [1,2,3,4,5]
gen2 <- newStdGen
shuffle gen2 [1,2,3,4,5]
-- using `split :: StdGen -> (StdGen, StdGen)`
gen3 <- newStdGen
let (_, gen4) = split gen3
shuffle gen3 [1,2,3,4,5]
let (_, gen5) = split gen4
shuffle gen4 [1,2,3,4,5]
StdGen
这意味着如果你想要不同的随机数,你要么必须做大量的簿记,要么留在 IO 中。由于引用透明性,这“有意义”再次 - 一组随机数必须彼此随机,因此您需要将信息从每个随机事件传递到下一个。
不过,这真的很烦人。我们能做得更好吗?
五。
好吧,通常我们需要的是一种让函数接收随机种子然后输出一些“随机”结果和下一个种子的方法。
withSeed :: (Seed -> a) -> Seed -> (a, Seed)
withSeed f s = (f s, splitSeed s)
结果类型withSeed f :: Seed -> (a, Seed)
是一个相当普遍的结果。让我们给它一个名字
newtype Random a = Random (Seed -> (a, Seed))
而且我们知道我们可以在 in 中创建有意义Seed
的 s IO
,所以有一个很明显的函数可以将Random
类型转换为IO
runRandom :: Random a -> IO a
runRandom (Random f) = do seed <- newSeed
let (result, _) = f seed
return result
现在感觉我们有了一些有用的东西——类型的随机值的概念a
,Random a
只是Seed
s 上的一个函数,它返回下一个Seed
,这样以后的Random
值就不会全部相同。我们甚至可以制作一些机器来组合随机值并Seed
自动传递
sequenceRandom :: Random a -> Random b -> Random b
sequenceRandom (Random fa) (Random fb) =
Random $ \seed -> let (_aValue, newSeed) = fa seed in fb newSeed
但这有点傻,因为我们只是扔掉_aValue
。让我们组合它们,使得第二个随机数实际上在很大程度上取决于第一个随机值。
bindRandom :: Random a -> (a -> Random b) -> Random b
bindRandom (Random fa) getRb =
Random $ \seed -> let (aValue, newSeed) = fa seed
(Random fb) = getRb aValue
in fb newSeed
我们还应该注意,我们可以对Random
值做“纯”的事情,例如,将随机数乘以 2:
randomTimesTwo :: Random Int -> Random Int
randomTimesTwo (Random f) = Random $ \seed -> let (value, newSeed) = f seed
in (value*2, newSeed)
我们可以将其抽象为 Functor 实例
instance Functor Random where
fmap f (Random step) = Random $ \seed -> let (value, newSeed) = step seed
in (f value, newSeed)
现在我们可以创建很酷的随机效果,比如布朗运动
brownianMotion :: Random [Int]
brownianMotion =
bindRandom random $ \x ->
fmap (\rest -> x : map (+x) rest) brownianMotion
六、
这触及了我一直在写的整个问题的核心。随机性可以IO
很好地存在于 monad 中,但它也可以作为更简单的Random
monad 单独存在。我们可以立即编写实例。
instance Monad Random where
return x = Random (\seed -> (x, seed))
rx >>= f = bindRandom rx f
因为它是一个单子,我们得到免费的do
符号
brownianMotion' = do x <- random
rest <- brownianMotion'
return $ x : map (+x) rest
你甚至可以幻想并称之为runRandom
单子同态,但这是一个非常不同的话题。
所以,回顾一下
Seed
引用透明语言中的随机性需要
- 购物车
Seed
很烦人
- “提升”和“绑定”随机值有一个共同的模式,可以
Seed
自然地路由 s
- 该模式形成一个单子
真正简短的回答是,您可能希望使用随机甚至MonadRandom来实现您的随机播放。通常,它们对于“采样”会派上用场。
干杯!