1。可测试
您无法轻松隔离其状态,尤其是在并行运行测试时。这使得在并行运行测试时无法以可预测的方式重现随机数序列。
2。可组合
您不能构建一个操作序列,首先使用已知值播种随机数,然后获取与该种子有关的序列(尤其是在并行运行时)。
虽然这可能与“可测试”重叠,但出于安全原因,它也可能是必要的,因此您不会意外地将状态从一个会话泄漏到另一个会话。
3。模块化
没有办法交换不同的随机算法。所有模块都必须使用相同的算法。这与“可组合”重叠,因为您也不能将生成器提供为(可能是隐式的)参数,而不是种子。
4。并行化
所有线程只有一种状态。在高度并发的情况下,这可能是一个严重的瓶颈。
一些示例代码
请不要在生产中使用此代码。这很笨拙,使用scalaz 或cats 您可以轻松构建更好的东西。通常,随机数生成器是一个共单子,它的状态被保存在单子中。
首先让我们通过提供不同的伪随机生成器和不同的种子来使代码模块化
// This is a commonly used pseudo-random generator
// https://en.wikipedia.org/wiki/Linear_congruential_generator
// This implementation is awful slow!
def lcg(multiplier : BigInt, increment : BigInt, dividend : BigInt)(seed : BigInt) =
(seed * multiplier + increment) mod dividend
// Some commonly used pseudo random number generators
def glibRand = lcg(multiplier = 1103515245, increment = 12345, dividend = 2^31) _
def vb6Rand = lcg(multiplier = 214013, increment = 2531011, dividend = 2^31) _
def javaRand = lcg(multiplier = 25214903917L, increment = 11, dividend = 2L^48) _
// This is really insecure, don't use it for anything serious!
def seedFromTime() = BigInt(System.nanoTime())
// I used a dice to determine this value, so it's random!
def randomSeed = 5
那么让我们构建一个可以组合函数的实现:
case class Random[T,U](val generator : T => T, val seed : T)(val currentValue : U){
def apply[V](f : (T,U) => V) : Random[T,V] = {
val nextRandom = generator(seed)
Random(generator, nextRandom)(f(nextRandom, currentValue))
}
def unsafeGetValue = currentValue
}
我们使用此构建块生成随机数列表并对一些随机数求和的几个示例:
val randomNumbers = Stream.iterate(Random(glibRand, seedFromTime())(List.empty[BigInt])){r : Random[BigInt, List[BigInt]] =>
r.apply{(rnd : BigInt, numbers : List[BigInt]) =>
rnd :: numbers
}
}
randomNumbers.take(10).last.unsafeGetValue
val randomSum = Stream.iterate(Random(glibRand, seedFromTime())(BigInt(0))) { r: Random[BigInt, BigInt] =>
r.apply(_ + _)
}
randomSum.take(100).last.unsafeGetValue
像往常一样,当我们想用函数式语言组合某些东西时,我们会组合函数来达到不同的目标。如果我们提供一个单子实现,我们还可以将函数与副作用结合起来。
在这种情况下(与往常一样),并行化和可测试性会自动进行。