1. Tested
You cannot easily isolate your state, especially when tests are executed in parallel. This makes it impossible to reproduce a sequence of random numbers in a predictable way in parallel testing.
2. Typed
You cannot create a sequence of operations that first sow a random number with a known value, and then get a sequence with respect to this seed (especially when running in parallel).
While this may intersect with “testable,” it may also be required for security reasons, so you do not accidentally leak from one session to another session.
3. Modular
It is not possible to swap different randomization algorithms. All modules must use the same algorithm. This overlaps with “compositional” in the sense that instead of seed you also cannot provide a generator as a (possibly implicit) parameter.
4. Parallelized
For all threads, there is only one state. In a very parallel situation, this can be a serious bottleneck.
Sample code example
Please do not use this code in the manufacturing process. This is pretty awkward and scalaz or cats you can easily build something better. Usually the random number generator is comonad, this state is executed in the monad.
First, make the code modular by providing different pseudo-random generators and different seeds
// 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 random! def randomSeed = 5
Then create an implementation that can create functions:
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 }
A few examples where we use these building blocks to create a list of random numbers and summarize some random numbers:
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
As usual, when we want to compose something in a functional language, we will compose functions to achieve different goals. If we provided a monadic implementation, we could also combine functions with side effects.
In this case, parallelization and verifiability are automatically performed (as usual).