It's easy to extend RandN with both new RNG algorithms and new distributions. RandN includes many utility methods to make creating new RNGs and distributions easy under the RandN.Implementation
namespace.
Implementing an RNG
To add a new RNG algorithm, you just need to implement IRng
. For example, here is the code for StepRng
, which is used in tests:
using RandN;
using RandN.Implementation;
public sealed class StepRng : IRng
{
public StepRng(UInt64 state) => State = state;
public UInt64 State { get; set; }
public UInt64 Increment { get; set; } = 1;
public void Fill(Span<Byte> buffer) => Filler.FillBytesViaNext(this, buffer);
public UInt32 NextUInt32() => (UInt32)NextUInt64();
public UInt64 NextUInt64()
{
var value = State;
State = unchecked(State + Increment);
return value;
}
}
If the algorithm is cryptographically secure, be sure to implement the marker interface ICryptoRng
as well.
You'll probably want to implement an RNG factory as well - either IReproducibleRngFactory
or IRngFactory
, depending on whether or not you want a reproducible RNG.
Reproducible RNG Factory
Implement IReproducibleRngFactory<TRng, TSeed>
, where TSeed
is the type the factory uses for RNG seeding. It has two methods - CreateSeed
to create a seed from another RNG, and Create
, which uses that seed to create an RNG. Mersenne Twister can use a 64-bit integer as a seed, as demonstrated below:
public readonly struct MersenneTwisterFactory : IReproducibleRngFactory<Mt1993764, UInt64>
{
public Mt1993764 Create(UInt64 seed)
{
return Mt1993764.Create(seed);
}
public UInt64 CreateSeed<TSeedingRng>(TSeedingRng seedingRng)
{
return seedingRng.NextUInt64();
}
}
Non-Reproducible RNG Factory
Implement IRngFactory<TRng>
- this is very simple since it just has one parameter-less method, Create()
. If the underlying RNG needs to be seeded, the best source is usually ThreadLocalRng
, which is guaranteed to be cryptographically secure and thread safe.
For example, here's a simplified implementation of StandardRng
's factory, which uses ChaCha8 internally:
public readonly struct StandardRngFactory : IRngFactory<ChaCha>
{
public ChaCha Create()
{
return ChaCha.GetChaCha8Factory().Create(ThreadLocalRng.Instance);
}
}
Implementing a Distribution
Implement IDistribution<TResult>
, where TResult
is the type that the distribution returns from sampling. There are two methods to implement Sample
, and TrySample
. How these methods are implemented depends on whether or not an attempt to sample can fail. For example, uniform Int32
distributions can fail if the range doesn't divide cleanly into a 32 bit number, and the RNG may need to be called multiple times until it returns a value in range. This may not be acceptable in some scenarios, so we provide TrySample
to avoid blocking too long the RNG.
Here's an example of a distribution that rejects all positive Int32
s; note how Sample
blocks until it gets an acceptable number:
public readonly struct NoPositiveInt32 : IDistribution<Int32>
{
public Boolean TrySample<TRng>(TRng rng, out Int32 result) where TRng : IRng
{
result = (Int32)rng.NextUInt32();
return result <= 0;
}
public Int32 Sample<TRng>(TRng rng) where TRng : IRng
{
while (true)
{
if (TrySample(rng, out Int32 result))
return result;
}
}
}
On the other hand, the algorithm used for floating point numbers such as Single
can scale any result to the target range. For example here's the implementation for a uniform distribution over the interval [0, 1). This time TrySample
simply calls Sample
, which doesn't block:
public readonly struct ClosedOpen : IDistribution<Single>
{
public Boolean TrySample<TRng>(TRng rng, out Single result) where TRng : IRng
{
result = Sample(rng);
return true;
}
public Single Sample<TRng>(TRng rng) where TRng : IRng
{
const Single scale = 1.0f / (1u << 24);
var random = rng.NextUInt32();
var value = random >> 8;
return scale * value;
}
}